| 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 | |
| 8 | import type {TrackErrorName, TrackEventName} from './eventNames'; |
| 9 | import type {TrackData, TrackDataWithEventName, TrackResult} from './types'; |
| 10 | |
| 11 | import {isPromise, randomId} from 'shared/utils'; |
| 12 | |
| 13 | type 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 | */ |
| 21 | export 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 | |