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