diff options
Diffstat (limited to 'shared/metrics-8/src/impressions')
| -rw-r--r-- | shared/metrics-8/src/impressions/constants.ts | 1 | ||||
| -rw-r--r-- | shared/metrics-8/src/impressions/index.ts | 252 |
2 files changed, 253 insertions, 0 deletions
diff --git a/shared/metrics-8/src/impressions/constants.ts b/shared/metrics-8/src/impressions/constants.ts new file mode 100644 index 0000000..0638f9e --- /dev/null +++ b/shared/metrics-8/src/impressions/constants.ts @@ -0,0 +1 @@ +export const IMPRESSION_CONTEXT_NAME = 'metrics:impression' as const; diff --git a/shared/metrics-8/src/impressions/index.ts b/shared/metrics-8/src/impressions/index.ts new file mode 100644 index 0000000..b904feb --- /dev/null +++ b/shared/metrics-8/src/impressions/index.ts @@ -0,0 +1,252 @@ +import type { Logger, LoggerFactory } from '@amp/web-apps-logger/src/types'; +import { IMPRESSION_CONTEXT_NAME } from './constants'; +import { createSvelteImpressionAction } from './utils/svelte/impressions-svelte-action'; +import type { + AppImpressionModel, + ImpressionSettings, + ImpressionsInstance, +} from './types'; +import type { + ImpressionObserver, + newInstanceWithMetricsConfig, +} from '@amp-metrics/mt-impressions-observer'; +import type { ClickstreamProcessor } from '@amp-metrics/mt-metricskit-processor-clickstream'; + +/** + * Adapter class to handle interactions with + * metricsKit impression observer. + */ +export class Impressions implements ImpressionsInstance { + private readonly logger: Logger; + private impressionObserverInstance: ImpressionObserver | undefined; + private hasInitialized: boolean = false; + private impressionDataMap: Map<HTMLElement, any> = new Map(); + private currentSnapshot: Record<string, unknown>[] = []; + private readonly impressionSettings: ImpressionSettings | undefined; + + constructor( + loggerFactory: LoggerFactory, + context: Map<string, unknown>, + settings?: ImpressionSettings, + ) { + this.logger = loggerFactory.loggerFor(IMPRESSION_CONTEXT_NAME); + this.impressionSettings = settings; + + context.set(IMPRESSION_CONTEXT_NAME, this); + } + + async init( + makeImpressionObserver: typeof newInstanceWithMetricsConfig, + clickStreamInstance: ClickstreamProcessor, + ) { + if (this.hasInitialized) { + this.logger.warn( + 'Ignoring, Impressions.init() can only be called once', + ); + return; + } + + const options = { root: document, rootMargin: '0px' }; + const config = clickStreamInstance.config; + const impressionObserver: ImpressionObserver = + await makeImpressionObserver(config, options); + + impressionObserver.setDelegate({ + extractImpressionInfo: (domNode: HTMLElement) => { + const dataMap = this.impressionDataMap; + const nodeMetricData = dataMap.get(domNode); + if (nodeMetricData) { + const impressionData = nodeMetricData.impressionMetrics; + impressionData.location = + clickStreamInstance.utils.eventFields.buildLocationStructure( + domNode, + (node: HTMLElement) => { + const metrics = dataMap.get(node); + if (metrics?.location) { + return metrics.location; + } + return; + }, + ); + return impressionData; + } else { + this.logger.warn('no impression data found for', domNode); + } + }, + }); + + this.impressionObserverInstance = impressionObserver; + this.impressionDataMap.forEach((_value, node) => { + this.logger.debug('observing deffered node', node); + this.impressionObserverInstance?.observe(node); + }); + this.hasInitialized = true; + + this.logger.debug('impressions initialized'); + } + + get settings() { + return this.impressionSettings; + } + + isEnabled(event: 'click' | 'exit' | 'impressions'): boolean { + if (this.impressionSettings?.captureType === 'jet') { + return ( + this.impressionSettings?.metricsKitEvents?.includes(event) ?? + false + ); + } + return true; + } + + consumeImpressions(): Record<string, unknown>[] | undefined { + if (this.hasInitialized) { + this.logger.debug('consuming impression metrics'); + return this.impressionObserverInstance?.consumeImpressions(); + } + this.logger.warn('impressions not avaiable yet'); + return; + } + + captureSnapshotImpression(): Record<string, unknown>[] | undefined { + const snapshot = + this.impressionObserverInstance?.snapshotImpressions() ?? []; + + // if the current page already transitioned. fallback to the snapshot we captured before transition + if (snapshot.length === 0) { + return this.getSnapshotImpression(); + } + + return snapshot; + } + + getSnapshotImpression(): Record<string, unknown>[] | undefined { + if (this.hasInitialized) { + return this.currentSnapshot; + } + this.logger.warn('impressions not avaiable yet'); + return; + } + + setCurrentSnapshot(): void { + if (this.hasInitialized) { + this.logger.debug('capturing impression snapshot'); + this.currentSnapshot = + this.impressionObserverInstance?.snapshotImpressions() ?? []; + } else { + this.logger.warn('impressions not avaiable yet'); + } + } + + get nodeList() { + const impressionClass = this; + + return new Proxy(impressionClass.impressionDataMap, { + get(target, prop, receiver) { + const orginalFn = Reflect.get(target, prop, receiver); + + // overriding 'set' to also be able to observe + if (prop === 'set') { + return ( + node: HTMLElement, + value: Record<string, unknown>, + ) => { + if (impressionClass.hasInitialized) { + impressionClass.logger.debug( + 'observing', + node, + value, + ); + + impressionClass.impressionObserverInstance?.observe( + node, + ); + } + + return orginalFn.bind(target)(node, value); + }; + } + + // overriding 'delete' to also be able to unobserve + if (prop === 'delete') { + return (node: HTMLElement) => { + if (impressionClass.hasInitialized) { + impressionClass.logger.debug('unobserve', node); + impressionClass.impressionObserverInstance?.unobserve( + node, + ); + } + + return orginalFn.bind(target)(node); + }; + } + + return orginalFn.bind(target); + }, + set(target, prop, value) { + return Reflect.set(target, prop, value); + }, + }); + } +} + +/** + * Server Noop for above + */ +class ServerNoopImpressions implements ImpressionsInstance { + readonly nodeList: WeakMap<any, any>; + constructor() { + this.nodeList = new WeakMap(); + } + setCurrentSnapshot(): void {} +} + +/** + * Gets the current Impression instance from the Svelte context. + * + * @return The current instance of Impression + */ +export function generateBrowserImpressionsContextGetter( + getContext: (context: string) => unknown, +): () => AppImpressionModel { + return function getImpressions(): AppImpressionModel { + const impressions = getContext(IMPRESSION_CONTEXT_NAME) as + | Impressions + | undefined; + + if (!impressions) { + const noopImpressions = new ServerNoopImpressions(); + return { + captureImpressions: (_node: any, _impressionsData: any) => { + return { + destroy() {}, + }; + }, + impressions: noopImpressions, + }; + } + + return { + captureImpressions: createSvelteImpressionAction(impressions), + impressions, + }; + }; +} + +/** + * Server No-op for generateImpressionsContextGetter + * + */ +export function generateServerImpressionsContextGetter( + _getContext: (context: string) => unknown, +): () => AppImpressionModel { + const impressions = new ServerNoopImpressions(); + return () => ({ + captureImpressions: (_node: any, _impressionsData: any) => { + return { + destroy() {}, + }; + }, + impressions, + }); +} |
