diff options
Diffstat (limited to 'shared/metrics-8/src/recorder/metricskit.ts')
| -rw-r--r-- | shared/metrics-8/src/recorder/metricskit.ts | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/shared/metrics-8/src/recorder/metricskit.ts b/shared/metrics-8/src/recorder/metricskit.ts new file mode 100644 index 0000000..9d724c9 --- /dev/null +++ b/shared/metrics-8/src/recorder/metricskit.ts @@ -0,0 +1,239 @@ +import type { MetricsEventRecorder } from '@jet/engine'; +import type { LintedMetricsEvent } from '@jet/environment/types/metrics'; +import type { Opt } from '@jet/environment/types/optional'; +import type { Logger, LoggerFactory } from '@amp/web-apps-logger'; + +import { METRICS_EVENT_TYPES } from '../constants'; + +import type { WebDelegates as WebDelegatesInstance } from '@amp-metrics/mt-metricskit-delegates-web'; +import type { ClickstreamProcessor as ClickstreamProcessorInstance } from '@amp-metrics/mt-metricskit-processor-clickstream'; +import type { Impressions } from '../impressions'; +import { sendToMetricsDevConsole } from '../utils/metrics-dev-console/setup-metrics-dev'; +import { getEventFieldsWithTopic } from '../utils/get-event-field-topic'; +import { eventType } from '../utils/metrics-dev-console/constants'; + +interface DeferredEvent { + event: LintedMetricsEvent; + topic: Opt<string>; +} + +type EventRecorder = WebDelegatesInstance['eventRecorder']; + +type MetricEventType = (typeof METRICS_EVENT_TYPES)[number]; + +export interface MetricKitConfig { + constraintProfiles: string[]; + topic: string; +} + +export class MetricsKitRecorder implements MetricsEventRecorder { + private readonly log: Logger; + private eventRecorder: EventRecorder | undefined; + private mtkit: ClickstreamProcessorInstance | undefined; + private recordedEventsCount: number; + private config: MetricKitConfig; + private readonly impressions: InstanceType<typeof Impressions> | undefined; + private enabled: boolean = true; + + /** + * Queues events prior to the mt-event-queue recorder being available + */ + private readonly deferredEvents: DeferredEvent[]; + + constructor( + loggerFactory: LoggerFactory, + config: MetricKitConfig, + impressions: InstanceType<typeof Impressions> | undefined, + ) { + this.log = loggerFactory.loggerFor('MetricsKitRecorder'); + this.deferredEvents = []; + this.recordedEventsCount = 0; + this.config = config; + this.impressions = impressions; + } + + record(event: LintedMetricsEvent, topic: Opt<string>): void { + topic = topic ?? this.config.topic; + if (this.isDisabled()) { + this.log.info( + `topic ${this.config.topic} is disabled following event not captured:`, + event, + ); + return; + } + + if (this.eventRecorder) { + const eventHandler = event.fields.eventType as MetricEventType; + const { pageId, pageType, pageContext } = event.fields; + if (!eventHandler) { + this.log.warn('No `eventType` found on event', event, topic); + return; + } else if (!METRICS_EVENT_TYPES.includes(eventHandler)) { + this.log.warn( + 'Invalid `eventType` found on event', + event, + topic, + ); + return; + } else if (!this.impressions && eventHandler === 'impressions') { + this.log.info( + 'Supressing impression event. Impressions not enabled', + ); + return; + } + + // when the user leaves a page to report the accumulated impressions for that page + if ( + (this.impressions?.isEnabled('exit') && + eventHandler === 'exit') || + (this.impressions?.isEnabled('click') && + event.fields.actionType === 'navigate') + ) { + // create + capture impressions + const accumulatedImpressions = + this.impressions.consumeImpressions(); + + const metricsData = this.mtkit?.eventHandlers[ + 'impressions' + ]?.metricsData(pageId, pageType, pageContext, { + impressions: accumulatedImpressions, + }); + + metricsData + ?.recordEvent(topic) + .then((data) => { + this.log.info( + 'impressions event captured', + data, + topic, + ); + sendToMetricsDevConsole( + data as { [key: string]: unknown }, + topic ?? '', + ); + }) + .catch((e) => { + this.log.warn( + 'failed to capture impression metrics', + e, + topic, + ); + }); + } + + let impressionsData = {}; + // snapshot impressions to include in click events + if ( + (this.impressions?.isEnabled('click') && + eventHandler === 'click') || + (this.impressions?.isEnabled('impressions') && + eventHandler === 'impressions') + ) { + const snapshotImpressions = + this.impressions.captureSnapshotImpression(); + impressionsData = { + impressions: snapshotImpressions, + }; + } + + const eventFields = getEventFieldsWithTopic(event, topic); + // click events are the only ones with different method signature + // https://github.pie.apple.com/amp-metrics/mt-metricskit/blob/7.3.5/src/metrics/event_handlers/click.js#L133 + const metricsDataArgs = + eventHandler === 'click' // TODO rdar://102438307 (JMOTW Clickstream – Pass targetElement to click events) + ? [ + pageId, + pageType, + pageContext, + null, + { ...eventFields, ...impressionsData }, + ] + : [pageId, pageType, pageContext, eventFields]; + + if (eventHandler === 'impressions') { + metricsDataArgs.push(impressionsData); + } + + let metricsData = this.mtkit?.eventHandlers[ + eventHandler + ]?.metricsData( + // @ts-expect-error TypeScript doesn't handle spreading the argument array well + ...metricsDataArgs, + ); + + metricsData + ?.recordEvent(topic) + .then((data) => { + this.log.info('MetricsKit event data', data, topic); + sendToMetricsDevConsole( + data as { [key: string]: unknown }, + topic ?? '', + ); + }) + .catch((e) => { + this.log.error( + 'MetricsKit failed to capture metric', + e, + topic, + ); + }); + + this.recordedEventsCount++; + + // on exit events we should flush all metrics + if (eventHandler === 'exit') { + this.eventRecorder?.flushUnreportedEvents?.(true); + sendToMetricsDevConsole( + { metricsDevType: eventType.FLUSH, status: 'SUCCESS' }, + topic, + ); + } + } else { + this.deferredEvents.push({ event, topic }); + } + } + + async flush(): Promise<number> { + await this.eventRecorder?.flushUnreportedEvents?.(false); + const count = this.recordedEventsCount; + this.recordedEventsCount = 0; + return count; + } + + setupEventRecorder( + eventRecorder: EventRecorder, + mtkit: ClickstreamProcessorInstance, + ): void { + this.eventRecorder = eventRecorder; + this.mtkit = mtkit; + this.deferredEvents.forEach(({ event, topic }) => + this.record(event, topic), + ); + this.deferredEvents.length = 0; + } + + isDisabled(): boolean { + return !this.enabled; + } + + enable(): void { + if (this.enabled) { + this.log.info( + `Clickstream topic ${this.config.topic} already enabled`, + ); + return; + } + + this.log.info(`Enabling clickstream topic ${this.config.topic}`); + this.enabled = true; + } + + disable(): void { + if (this.isDisabled()) { + return; + } + + this.log.info(`Disabling clickstream topic ${this.config.topic}`); + this.enabled = false; + } +} |
