4.0 KB128 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {TrackErrorName, TrackEventName} from './eventNames';
9import type {TrackData, TrackDataWithEventName, TrackResult} from './types';
10
11import {isPromise, randomId} from 'shared/utils';
12
13type SendData<T> = (data: TrackDataWithEventName, context: T) => void;
14
15/**
16 * Handles analytics tracking for both the server and client sides.
17 * Each instance provides a callback to define how to send data for logging.
18 * - The client sends a message to the server to further process
19 * - The server sends the data to finally get processed
20 */
21export class Tracker<T> {
22 constructor(
23 private sendData: SendData<T>,
24 public context: T,
25 ) {}
26
27 /**
28 * Record an analytics error event `eventName`.
29 * Like `track`, but also fills out `errorName` and `errorMessage`.
30 */
31 error(
32 eventName: TrackEventName,
33 errorName: TrackErrorName,
34 error: Error | string | undefined,
35 data?: TrackData,
36 ): void {
37 const errorMessage = error instanceof Error ? error.message || String(error) : error;
38 return this.track(eventName, {...(data ?? {}), errorMessage, errorName});
39 }
40
41 /**
42 * Wrap a function with `track()`.
43 * If the function throws (or rejects for async), the error is tracked.
44 * The execution time is measured and included in the `duration` field.
45 */
46 public operation<T>(
47 eventName: TrackEventName,
48 errorName: TrackErrorName,
49 data: TrackData | undefined,
50 operation: (parent: TrackResult) => T,
51 ): T {
52 const startTime = Date.now();
53 const id = data?.id ?? randomId();
54 try {
55 const result = operation({parentId: id});
56 if (isPromise(result)) {
57 return result
58 .then(finalResult => {
59 const endTime = Date.now();
60 const duration = endTime - startTime;
61 this.track(eventName, {...(data ?? {}), duration, id});
62 return finalResult;
63 })
64 .catch(err => {
65 const endTime = Date.now();
66 const duration = endTime - startTime;
67 this.error(eventName, errorName, err, {
68 ...(data ?? {}),
69 duration,
70 id,
71 });
72 return Promise.reject(err);
73 }) as unknown as T;
74 } else {
75 const endTime = Date.now();
76 const duration = endTime - startTime;
77 this.track(eventName, {...(data ?? {}), duration, id});
78 return result;
79 }
80 } catch (err) {
81 const endTime = Date.now();
82 const duration = endTime - startTime;
83 this.error(eventName, errorName, err as Error | string, {
84 ...(data ?? {}),
85 duration,
86 id,
87 });
88 throw err;
89 }
90 }
91
92 /**
93 * Track an event, then return a child `Tracker` that correlates future track calls via the `parentId` field.
94 * This way, you can recover the relationship of a tree of events.
95 */
96 public trackAsParent(eventName: TrackEventName, data?: TrackData): Tracker<{parentId: string}> {
97 const id = data?.id ?? randomId();
98 this.trackData({...data, eventName, id});
99 const childTracker = new Tracker((childData, ctx) => this.trackData({...childData, ...ctx}), {
100 parentId: id,
101 });
102 return childTracker;
103 }
104
105 /**
106 * Record an analytics event `eventName`.
107 * Optionally provide additional fields, like arbitrary JSON `extras`.
108 */
109 public track(eventName: TrackEventName, data?: Readonly<TrackData>): void {
110 return this.trackData({...data, eventName});
111 }
112
113 /**
114 * Record analytics event with filled in data struct.
115 * `track()` is an easier to use wrapper around this function.
116 */
117 public trackData(data: TrackDataWithEventName): void {
118 const id = data?.id ?? randomId();
119 const timestamp = data?.timestamp ?? Date.now();
120 const trackData: TrackDataWithEventName = {
121 timestamp,
122 id,
123 ...(data ?? {}),
124 };
125 this.sendData(trackData, this.context);
126 }
127}
128