summaryrefslogtreecommitdiff
path: root/shared/metrics-8/src/impressions/index.ts
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /shared/metrics-8/src/impressions/index.ts
init commit
Diffstat (limited to 'shared/metrics-8/src/impressions/index.ts')
-rw-r--r--shared/metrics-8/src/impressions/index.ts252
1 files changed, 252 insertions, 0 deletions
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,
+ });
+}