addons/isl-server/src/analytics/tracker.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {TrackErrorName, TrackEventName} from './eventNames';
b69ab319import type {TrackData, TrackDataWithEventName, TrackResult} from './types';
b69ab3110
b69ab3111import {isPromise, randomId} from 'shared/utils';
b69ab3112
b69ab3113type SendData<T> = (data: TrackDataWithEventName, context: T) => void;
b69ab3114
b69ab3115/**
b69ab3116 * Handles analytics tracking for both the server and client sides.
b69ab3117 * Each instance provides a callback to define how to send data for logging.
b69ab3118 * - The client sends a message to the server to further process
b69ab3119 * - The server sends the data to finally get processed
b69ab3120 */
b69ab3121export class Tracker<T> {
b69ab3122 constructor(
b69ab3123 private sendData: SendData<T>,
b69ab3124 public context: T,
b69ab3125 ) {}
b69ab3126
b69ab3127 /**
b69ab3128 * Record an analytics error event `eventName`.
b69ab3129 * Like `track`, but also fills out `errorName` and `errorMessage`.
b69ab3130 */
b69ab3131 error(
b69ab3132 eventName: TrackEventName,
b69ab3133 errorName: TrackErrorName,
b69ab3134 error: Error | string | undefined,
b69ab3135 data?: TrackData,
b69ab3136 ): void {
b69ab3137 const errorMessage = error instanceof Error ? error.message || String(error) : error;
b69ab3138 return this.track(eventName, {...(data ?? {}), errorMessage, errorName});
b69ab3139 }
b69ab3140
b69ab3141 /**
b69ab3142 * Wrap a function with `track()`.
b69ab3143 * If the function throws (or rejects for async), the error is tracked.
b69ab3144 * The execution time is measured and included in the `duration` field.
b69ab3145 */
b69ab3146 public operation<T>(
b69ab3147 eventName: TrackEventName,
b69ab3148 errorName: TrackErrorName,
b69ab3149 data: TrackData | undefined,
b69ab3150 operation: (parent: TrackResult) => T,
b69ab3151 ): T {
b69ab3152 const startTime = Date.now();
b69ab3153 const id = data?.id ?? randomId();
b69ab3154 try {
b69ab3155 const result = operation({parentId: id});
b69ab3156 if (isPromise(result)) {
b69ab3157 return result
b69ab3158 .then(finalResult => {
b69ab3159 const endTime = Date.now();
b69ab3160 const duration = endTime - startTime;
b69ab3161 this.track(eventName, {...(data ?? {}), duration, id});
b69ab3162 return finalResult;
b69ab3163 })
b69ab3164 .catch(err => {
b69ab3165 const endTime = Date.now();
b69ab3166 const duration = endTime - startTime;
b69ab3167 this.error(eventName, errorName, err, {
b69ab3168 ...(data ?? {}),
b69ab3169 duration,
b69ab3170 id,
b69ab3171 });
b69ab3172 return Promise.reject(err);
b69ab3173 }) as unknown as T;
b69ab3174 } else {
b69ab3175 const endTime = Date.now();
b69ab3176 const duration = endTime - startTime;
b69ab3177 this.track(eventName, {...(data ?? {}), duration, id});
b69ab3178 return result;
b69ab3179 }
b69ab3180 } catch (err) {
b69ab3181 const endTime = Date.now();
b69ab3182 const duration = endTime - startTime;
b69ab3183 this.error(eventName, errorName, err as Error | string, {
b69ab3184 ...(data ?? {}),
b69ab3185 duration,
b69ab3186 id,
b69ab3187 });
b69ab3188 throw err;
b69ab3189 }
b69ab3190 }
b69ab3191
b69ab3192 /**
b69ab3193 * Track an event, then return a child `Tracker` that correlates future track calls via the `parentId` field.
b69ab3194 * This way, you can recover the relationship of a tree of events.
b69ab3195 */
b69ab3196 public trackAsParent(eventName: TrackEventName, data?: TrackData): Tracker<{parentId: string}> {
b69ab3197 const id = data?.id ?? randomId();
b69ab3198 this.trackData({...data, eventName, id});
b69ab3199 const childTracker = new Tracker((childData, ctx) => this.trackData({...childData, ...ctx}), {
b69ab31100 parentId: id,
b69ab31101 });
b69ab31102 return childTracker;
b69ab31103 }
b69ab31104
b69ab31105 /**
b69ab31106 * Record an analytics event `eventName`.
b69ab31107 * Optionally provide additional fields, like arbitrary JSON `extras`.
b69ab31108 */
b69ab31109 public track(eventName: TrackEventName, data?: Readonly<TrackData>): void {
b69ab31110 return this.trackData({...data, eventName});
b69ab31111 }
b69ab31112
b69ab31113 /**
b69ab31114 * Record analytics event with filled in data struct.
b69ab31115 * `track()` is an easier to use wrapper around this function.
b69ab31116 */
b69ab31117 public trackData(data: TrackDataWithEventName): void {
b69ab31118 const id = data?.id ?? randomId();
b69ab31119 const timestamp = data?.timestamp ?? Date.now();
b69ab31120 const trackData: TrackDataWithEventName = {
b69ab31121 timestamp,
b69ab31122 id,
b69ab31123 ...(data ?? {}),
b69ab31124 };
b69ab31125 this.sendData(trackData, this.context);
b69ab31126 }
b69ab31127}