@dxos/observability 0.3.11-main.cf49a01

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +26 -0
  3. package/dist/lib/browser/chunk-2J5YS65Q.mjs +81 -0
  4. package/dist/lib/browser/chunk-2J5YS65Q.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-6KUJPLEH.mjs +293 -0
  6. package/dist/lib/browser/chunk-6KUJPLEH.mjs.map +7 -0
  7. package/dist/lib/browser/datadog/index.mjs +9 -0
  8. package/dist/lib/browser/datadog/index.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +557 -0
  10. package/dist/lib/browser/index.mjs.map +7 -0
  11. package/dist/lib/browser/meta.json +1 -0
  12. package/dist/lib/browser/sentry/index.mjs +25 -0
  13. package/dist/lib/browser/sentry/index.mjs.map +7 -0
  14. package/dist/lib/node/chunk-FSAOBKO2.cjs +352 -0
  15. package/dist/lib/node/chunk-FSAOBKO2.cjs.map +7 -0
  16. package/dist/lib/node/chunk-VDBBIYH6.cjs +101 -0
  17. package/dist/lib/node/chunk-VDBBIYH6.cjs.map +7 -0
  18. package/dist/lib/node/datadog/index.cjs +31 -0
  19. package/dist/lib/node/datadog/index.cjs.map +7 -0
  20. package/dist/lib/node/index.cjs +630 -0
  21. package/dist/lib/node/index.cjs.map +7 -0
  22. package/dist/lib/node/meta.json +1 -0
  23. package/dist/lib/node/sentry/index.cjs +45 -0
  24. package/dist/lib/node/sentry/index.cjs.map +7 -0
  25. package/dist/types/src/cli-observability-secrets.json +7 -0
  26. package/dist/types/src/datadog/browser.d.ts +8 -0
  27. package/dist/types/src/datadog/browser.d.ts.map +1 -0
  28. package/dist/types/src/datadog/browser.js +22 -0
  29. package/dist/types/src/datadog/browser.js.map +1 -0
  30. package/dist/types/src/datadog/index.d.ts +3 -0
  31. package/dist/types/src/datadog/index.d.ts.map +1 -0
  32. package/dist/types/src/datadog/index.js +22 -0
  33. package/dist/types/src/datadog/index.js.map +1 -0
  34. package/dist/types/src/datadog/metrics.d.ts +16 -0
  35. package/dist/types/src/datadog/metrics.d.ts.map +1 -0
  36. package/dist/types/src/datadog/metrics.js +56 -0
  37. package/dist/types/src/datadog/metrics.js.map +1 -0
  38. package/dist/types/src/datadog/node.d.ts +7 -0
  39. package/dist/types/src/datadog/node.d.ts.map +1 -0
  40. package/dist/types/src/datadog/node.js +22 -0
  41. package/dist/types/src/datadog/node.js.map +1 -0
  42. package/dist/types/src/index.d.ts +2 -0
  43. package/dist/types/src/index.d.ts.map +1 -0
  44. package/dist/types/src/index.js +21 -0
  45. package/dist/types/src/index.js.map +1 -0
  46. package/dist/types/src/observability.d.ts +85 -0
  47. package/dist/types/src/observability.d.ts.map +1 -0
  48. package/dist/types/src/observability.js +352 -0
  49. package/dist/types/src/observability.js.map +1 -0
  50. package/dist/types/src/segment/browser.d.ts +9 -0
  51. package/dist/types/src/segment/browser.d.ts.map +1 -0
  52. package/dist/types/src/segment/browser.js +71 -0
  53. package/dist/types/src/segment/browser.js.map +1 -0
  54. package/dist/types/src/segment/index.d.ts +3 -0
  55. package/dist/types/src/segment/index.d.ts.map +1 -0
  56. package/dist/types/src/segment/index.js +22 -0
  57. package/dist/types/src/segment/index.js.map +1 -0
  58. package/dist/types/src/segment/node.d.ts +19 -0
  59. package/dist/types/src/segment/node.d.ts.map +1 -0
  60. package/dist/types/src/segment/node.js +88 -0
  61. package/dist/types/src/segment/node.js.map +1 -0
  62. package/dist/types/src/segment/types.d.ts +22 -0
  63. package/dist/types/src/segment/types.d.ts.map +1 -0
  64. package/dist/types/src/segment/types.js +6 -0
  65. package/dist/types/src/segment/types.js.map +1 -0
  66. package/dist/types/src/sentry/browser.d.ts +31 -0
  67. package/dist/types/src/sentry/browser.d.ts.map +1 -0
  68. package/dist/types/src/sentry/browser.js +112 -0
  69. package/dist/types/src/sentry/browser.js.map +1 -0
  70. package/dist/types/src/sentry/index.d.ts +4 -0
  71. package/dist/types/src/sentry/index.d.ts.map +1 -0
  72. package/dist/types/src/sentry/index.js +26 -0
  73. package/dist/types/src/sentry/index.js.map +1 -0
  74. package/dist/types/src/sentry/node.d.ts +32 -0
  75. package/dist/types/src/sentry/node.d.ts.map +1 -0
  76. package/dist/types/src/sentry/node.js +169 -0
  77. package/dist/types/src/sentry/node.js.map +1 -0
  78. package/dist/types/src/sentry/node.test.d.ts +2 -0
  79. package/dist/types/src/sentry/node.test.d.ts.map +1 -0
  80. package/dist/types/src/sentry/node.test.js +66 -0
  81. package/dist/types/src/sentry/node.test.js.map +1 -0
  82. package/dist/types/src/sentry/sentry.test.d.ts +2 -0
  83. package/dist/types/src/sentry/sentry.test.d.ts.map +1 -0
  84. package/dist/types/src/sentry/sentry.test.js +60 -0
  85. package/dist/types/src/sentry/sentry.test.js.map +1 -0
  86. package/dist/types/src/sentry/tracing.d.ts +5 -0
  87. package/dist/types/src/sentry/tracing.d.ts.map +1 -0
  88. package/dist/types/src/sentry/tracing.js +141 -0
  89. package/dist/types/src/sentry/tracing.js.map +1 -0
  90. package/dist/types/src/sentry/tracing.test.d.ts +2 -0
  91. package/dist/types/src/sentry/tracing.test.d.ts.map +1 -0
  92. package/dist/types/src/sentry/tracing.test.js +76 -0
  93. package/dist/types/src/sentry/tracing.test.js.map +1 -0
  94. package/dist/types/src/sentry/types.d.ts +18 -0
  95. package/dist/types/src/sentry/types.d.ts.map +1 -0
  96. package/dist/types/src/sentry/types.js +6 -0
  97. package/dist/types/src/sentry/types.js.map +1 -0
  98. package/dist/types/src/testing/index.d.ts +2 -0
  99. package/dist/types/src/testing/index.d.ts.map +1 -0
  100. package/dist/types/src/testing/index.js +21 -0
  101. package/dist/types/src/testing/index.js.map +1 -0
  102. package/dist/types/src/testing/testkit/browser.d.ts +2 -0
  103. package/dist/types/src/testing/testkit/browser.d.ts.map +1 -0
  104. package/dist/types/src/testing/testkit/browser.js +13 -0
  105. package/dist/types/src/testing/testkit/browser.js.map +1 -0
  106. package/dist/types/src/testing/testkit/index.d.ts +2 -0
  107. package/dist/types/src/testing/testkit/index.d.ts.map +1 -0
  108. package/dist/types/src/testing/testkit/index.js +12 -0
  109. package/dist/types/src/testing/testkit/index.js.map +1 -0
  110. package/dist/types/src/util.d.ts +19 -0
  111. package/dist/types/src/util.d.ts.map +1 -0
  112. package/dist/types/src/util.js +42 -0
  113. package/dist/types/src/util.js.map +1 -0
  114. package/package.json +94 -0
  115. package/src/cli-observability-secrets.json +7 -0
  116. package/src/datadog/browser.ts +21 -0
  117. package/src/datadog/index.ts +6 -0
  118. package/src/datadog/metrics.ts +49 -0
  119. package/src/datadog/node.ts +19 -0
  120. package/src/index.ts +5 -0
  121. package/src/observability.ts +442 -0
  122. package/src/segment/browser.ts +67 -0
  123. package/src/segment/index.ts +6 -0
  124. package/src/segment/node.ts +91 -0
  125. package/src/segment/types.ts +27 -0
  126. package/src/sentry/browser.ts +100 -0
  127. package/src/sentry/index.ts +9 -0
  128. package/src/sentry/node.test.ts +46 -0
  129. package/src/sentry/node.ts +153 -0
  130. package/src/sentry/sentry.test.ts +39 -0
  131. package/src/sentry/tracing.test.ts +62 -0
  132. package/src/sentry/tracing.ts +153 -0
  133. package/src/sentry/types.ts +22 -0
  134. package/src/testing/index.ts +5 -0
  135. package/src/testing/testkit/browser.ts +8 -0
  136. package/src/testing/testkit/index.ts +7 -0
  137. package/src/testing/testkit/shims.d.ts +5 -0
  138. package/src/util.ts +49 -0
@@ -0,0 +1,442 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Event, scheduleTaskInterval } from '@dxos/async';
6
+ import { type Client, type Config } from '@dxos/client';
7
+ import { type Space } from '@dxos/client-protocol';
8
+ import { Context } from '@dxos/context';
9
+ import { invariant } from '@dxos/invariant';
10
+ import { log } from '@dxos/log';
11
+ import { ConnectionState } from '@dxos/network-manager';
12
+ import { DeviceKind, type NetworkStatus, Platform } from '@dxos/protocols/proto/dxos/client/services';
13
+ import { isNode } from '@dxos/util';
14
+
15
+ import buildSecrets from './cli-observability-secrets.json';
16
+ import { DatadogMetrics } from './datadog';
17
+ import { SegmentTelemetry, type EventOptions, type PageOptions } from './segment';
18
+ import {
19
+ captureException as sentryCaptureException,
20
+ enableSentryLogProcessor,
21
+ configureTracing as sentryConfigureTracing,
22
+ init as sentryInit,
23
+ type InitOptions,
24
+ setTag as sentrySetTag,
25
+ } from './sentry';
26
+ import { mapSpaces } from './util';
27
+
28
+ const SPACE_METRICS_MIN_INTERVAL = 1000 * 60;
29
+ // const DATADOG_IDLE_INTERVAL = 1000 * 60 * 5;
30
+ const DATADOG_IDLE_INTERVAL = 1000 * 60 * 1;
31
+
32
+ // TODO(nf): add allowlist for telemetry tags?
33
+ const ERROR_TAGS = ['identityKey', 'username', 'deviceKey', 'group'];
34
+
35
+ // Secrets? EnvironmentConfig?
36
+
37
+ export type ObservabilitySecrets = {
38
+ DX_ENVIRONMENT: string | null;
39
+ DX_RELEASE: string | null;
40
+ SENTRY_DESTINATION: string | null;
41
+ TELEMETRY_API_KEY: string | null;
42
+ IPDATA_API_KEY: string | null;
43
+ DATADOG_API_KEY: string | null;
44
+ DATADOG_APP_KEY: string | null;
45
+ };
46
+
47
+ export type Mode = 'basic' | 'full' | 'disabled';
48
+
49
+ export type ObservabilityOptions = {
50
+ /// The webapp (e.g. 'composer.dxos.org'), 'cli', or 'agent'.
51
+ namespace: string;
52
+ // TODO(nf): make platform a required extension?
53
+ // platform: Platform;
54
+ config?: Config;
55
+ secrets?: Record<string, string>;
56
+ group?: string;
57
+ mode?: Mode;
58
+
59
+ telemetry?: {
60
+ batchSize?: number;
61
+ };
62
+
63
+ errorLog?: {
64
+ sentryInitOptions?: InitOptions;
65
+ };
66
+ logProcessor?: boolean;
67
+ };
68
+
69
+ /*
70
+ * Observability provides a common interface for error logging, metrics, and telemetry.
71
+ * It currently provides these capabilities using Sentry, Datadog, and Segment.
72
+ */
73
+ export class Observability {
74
+ // TODO(wittjosiah): Generic metrics interface.
75
+ private _metrics?: DatadogMetrics;
76
+ // TODO(wittjosiah): Generic telemetry interface.
77
+ private _telemetryBatchSize: number;
78
+ private _telemetry?: SegmentTelemetry;
79
+ // TODO(wittjosiah): Generic error logging interface.
80
+ private _errorReportingOptions?: InitOptions;
81
+ private _errorLogProcessor: boolean;
82
+
83
+ private _secrets: ObservabilitySecrets;
84
+ private _namespace: string;
85
+ private _config?: Config;
86
+ private _mode: Mode = 'disabled';
87
+ private _group?: string;
88
+ // TODO(nf): accept upstream context?
89
+ private _ctx = new Context();
90
+ private _tags = new Map<string, string>();
91
+
92
+ // TODO(nf): make platform a required extension?
93
+ constructor({ namespace, config, secrets, group, mode, telemetry, errorLog, logProcessor }: ObservabilityOptions) {
94
+ this._namespace = namespace;
95
+ this._config = config;
96
+ this._mode = mode ?? 'disabled';
97
+ this._group = group;
98
+ this._secrets = this._loadSecrets(config, secrets);
99
+ this._telemetryBatchSize = telemetry?.batchSize ?? 30;
100
+ this._errorReportingOptions = errorLog?.sentryInitOptions;
101
+ this._errorLogProcessor = logProcessor ?? false;
102
+
103
+ if (this._group) {
104
+ this.setTag('group', this._group);
105
+ }
106
+ this.setTag('namespace', this._namespace);
107
+
108
+ if (this._mode === 'full') {
109
+ // TODO(nf): set group and hostname?
110
+ }
111
+ }
112
+
113
+ private _loadSecrets(config: Config | undefined, secrets?: Record<string, string>) {
114
+ if (isNode()) {
115
+ return buildSecrets as ObservabilitySecrets;
116
+ } else {
117
+ invariant(config, 'runtime config is required');
118
+ log('config', { rtc: this._secrets, config });
119
+ return {
120
+ DX_ENVIRONMENT: config.get('runtime.app.env.DX_ENVIRONMENT'),
121
+ DX_RELEASE: config.get('runtime.app.env.DX_RELEASE'),
122
+ SENTRY_DESTINATION: config.get('runtime.app.env.DX_SENTRY_DESTINATION'),
123
+ TELEMETRY_API_KEY: config.get('runtime.app.env.DX_TELEMETRY_API_KEY'),
124
+ IPDATA_API_KEY: config.get('runtime.app.env.DX_IPDATA_API_KEY'),
125
+ DATADOG_API_KEY: config.get('runtime.app.env.DX_DATADOG_API_KEY'),
126
+ DATADOG_APP_KEY: config.get('runtime.app.env.DX_DATADOG_APP_KEY'),
127
+ ...secrets,
128
+ };
129
+ }
130
+ }
131
+
132
+ initialize() {
133
+ this._initMetrics();
134
+ this._initTelemetry();
135
+ this._initErrorLogs();
136
+ }
137
+
138
+ async close() {
139
+ await this._ctx.dispose();
140
+
141
+ // TODO(wittjosiah): Remove telemetry, etc. scripts.
142
+ }
143
+
144
+ setMode(mode: Mode) {
145
+ this._mode = mode;
146
+ }
147
+
148
+ get mode() {
149
+ return this._mode;
150
+ }
151
+
152
+ get enabled() {
153
+ return this._mode !== 'disabled';
154
+ }
155
+
156
+ //
157
+ // Tags
158
+ //
159
+
160
+ /**
161
+ * Set a tag for all observability services.
162
+ */
163
+ setTag(key: string, value: string) {
164
+ if (this.enabled && ERROR_TAGS.includes(key)) {
165
+ sentrySetTag(key, value);
166
+ }
167
+
168
+ this._tags.set(key, value);
169
+ }
170
+
171
+ // TODO(nf): Combine with setDeviceTags.
172
+ async setIdentityTags(client: Client) {
173
+ client.services.services.IdentityService!.queryIdentity().subscribe((idqr) => {
174
+ if (!idqr?.identity?.identityKey) {
175
+ log('empty response from identity service', { idqr });
176
+ return;
177
+ }
178
+
179
+ // TODO(nf): check mode
180
+ // TODO(nf): cardinality
181
+ this.setTag('identityKey', idqr?.identity?.identityKey.truncate());
182
+ if (idqr?.identity?.profile?.displayName) {
183
+ this.setTag('username', idqr?.identity?.profile?.displayName);
184
+ }
185
+ });
186
+ }
187
+
188
+ async setDeviceTags(client: Client) {
189
+ client.services.services.DevicesService!.queryDevices().subscribe((dqr) => {
190
+ if (!dqr || !dqr.devices || dqr.devices.length === 0) {
191
+ log('empty response from device service', { device: dqr });
192
+ return;
193
+ }
194
+ invariant(dqr, 'empty response from device service');
195
+
196
+ const thisDevice = dqr.devices.find((device) => device.kind === DeviceKind.CURRENT);
197
+ if (!thisDevice) {
198
+ log('no current device', { device: dqr });
199
+ return;
200
+ }
201
+ this.setTag('deviceKey', thisDevice.deviceKey.truncate());
202
+ if (thisDevice.profile?.label) {
203
+ this.setTag('deviceProfile', thisDevice.profile.label);
204
+ }
205
+ });
206
+ }
207
+
208
+ //
209
+ // Metrics
210
+ //
211
+
212
+ private _initMetrics() {
213
+ if (this.enabled && this._secrets.DATADOG_API_KEY) {
214
+ this._metrics = new DatadogMetrics({
215
+ apiKey: this._secrets.DATADOG_API_KEY,
216
+ getTags: () => this._tags,
217
+ // TODO(nf): move/refactor from telementryContext, needed to read CORS proxy
218
+ config: this._config!,
219
+ });
220
+ } else {
221
+ log('datadog disabled');
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Gauge metric.
227
+ *
228
+ * The default implementation uses Datadog.
229
+ */
230
+ gauge(name: string, value: number | any, extraTags?: any) {
231
+ this._metrics?.gauge(name, value, extraTags);
232
+ }
233
+
234
+ // TODO(nf): Refactor into ObservabilityExtensions.
235
+
236
+ startNetworkMetrics(client: Client) {
237
+ const updateSignalMetrics = new Event<NetworkStatus>();
238
+
239
+ // const lcsh = (csp as LocalClientServices).host;
240
+ updateSignalMetrics.on(this._ctx, async (networkStatus) => {
241
+ log('send signal metrics');
242
+ (networkStatus.signaling as NetworkStatus.Signal[]).forEach(({ server, state }) => {
243
+ this.gauge('dxos.client.network.signal.connectionState', state, { server });
244
+ });
245
+
246
+ let swarmCount = 0;
247
+ const connectionStates = new Map<string, number>();
248
+ for (const state in ConnectionState) {
249
+ connectionStates.set(state, 0);
250
+ }
251
+
252
+ let totalReadBufferSize = 0;
253
+ let totalWriteBufferSize = 0;
254
+ let totalChannelBufferSize = 0;
255
+
256
+ networkStatus.connectionInfo?.forEach((connectionInfo) => {
257
+ swarmCount++;
258
+
259
+ for (const conn of connectionInfo.connections ?? []) {
260
+ connectionStates.set(conn.state, (connectionStates.get(conn.state) ?? 0) + 1);
261
+ totalReadBufferSize += conn.readBufferSize ?? 0;
262
+ totalWriteBufferSize += conn.writeBufferSize ?? 0;
263
+
264
+ for (const stream of conn.streams ?? []) {
265
+ totalChannelBufferSize += stream.writeBufferSize ?? 0;
266
+ }
267
+ }
268
+
269
+ this.gauge('dxos.client.network.swarm.count', swarmCount);
270
+ for (const state in ConnectionState) {
271
+ this.gauge('dxos.client.network.connection.count', connectionStates.get(state) ?? 0, { state });
272
+ }
273
+ this.gauge('dxox.client.network.totalReadBufferSize', totalReadBufferSize);
274
+ this.gauge('dxos.client.network.totalWriteBufferSize', totalWriteBufferSize);
275
+ this.gauge('dxos.client.network.totalChannelBufferSize', totalChannelBufferSize);
276
+ });
277
+ });
278
+
279
+ client.services.services.NetworkService?.queryStatus().subscribe((networkStatus) => {
280
+ updateSignalMetrics.emit(networkStatus);
281
+ });
282
+
283
+ // scheduleTaskInterval(ctx, async () => updateSignalMetrics.emit(), DATADOG_IDLE_INTERVAL);
284
+ }
285
+
286
+ startSpacesMetrics(client: Client) {
287
+ let spaces = client.spaces.get();
288
+ const subscriptions = new Map<string, { unsubscribe: () => void }>();
289
+ this._ctx.onDispose(() => subscriptions.forEach((subscription) => subscription.unsubscribe()));
290
+
291
+ const updateSpaceMetrics = new Event<Space>().debounce(SPACE_METRICS_MIN_INTERVAL);
292
+ updateSpaceMetrics.on(this._ctx, async (space) => {
293
+ log('send space update');
294
+ for (const sp of mapSpaces(spaces, { truncateKeys: true })) {
295
+ this.gauge('dxos.client.space.members', sp.members, { key: sp.key });
296
+ this.gauge('dxos.client.space.objects', sp.objects, { key: sp.key });
297
+ this.gauge('dxos.client.space.epoch', sp.epoch, { key: sp.key });
298
+ this.gauge('dxos.client.space.currentDataMutations', sp.currentDataMutations, { key: sp.key });
299
+ }
300
+ });
301
+
302
+ const subscribeToSpaceUpdate = (space: Space) =>
303
+ space.pipeline.subscribe({
304
+ next: () => {
305
+ updateSpaceMetrics.emit();
306
+ },
307
+ });
308
+
309
+ spaces.forEach((space) => {
310
+ subscriptions.set(space.key.toHex(), subscribeToSpaceUpdate(space));
311
+ });
312
+
313
+ client.spaces.subscribe({
314
+ next: async () => {
315
+ spaces = client.spaces.get();
316
+ // spaces = await this.getSpaces(this._agent.client);
317
+ spaces
318
+ .filter((space) => !subscriptions.has(space.key.toHex()))
319
+ .forEach((space) => {
320
+ subscriptions.set(space.key.toHex(), subscribeToSpaceUpdate(space));
321
+ });
322
+ },
323
+ });
324
+
325
+ scheduleTaskInterval(this._ctx, async () => updateSpaceMetrics.emit(), DATADOG_IDLE_INTERVAL);
326
+ }
327
+
328
+ async startRuntimeMetrics(client: Client, frequency: number = DATADOG_IDLE_INTERVAL) {
329
+ const platform = await client.services.services.SystemService?.getPlatform();
330
+ invariant(platform, 'platform is required');
331
+
332
+ this.setTag('platform_type', Platform.PLATFORM_TYPE[platform.type as number].toLowerCase());
333
+ if (this._mode === 'full') {
334
+ // platform[foo] does not work?
335
+ if (platform.platform) {
336
+ this.setTag('platform', platform.platform);
337
+ }
338
+ if (platform.arch) {
339
+ this.setTag('arch', platform.arch);
340
+ }
341
+ if (platform.runtime) {
342
+ this.setTag('runtime', platform.runtime);
343
+ }
344
+ }
345
+ scheduleTaskInterval(
346
+ this._ctx,
347
+ async () => {
348
+ log('platform');
349
+ client.services.services.SystemService?.getPlatform()
350
+ .then((platform) => {
351
+ log('platform', { platform });
352
+ if (platform.memory) {
353
+ this.gauge('dxos.client.runtime.rss', platform.memory.rss);
354
+ this.gauge('dxos.client.runtime.heapTotal', platform.memory.heapTotal);
355
+ this.gauge('dxos.client.runtime.heapUsed', platform.memory.heapUsed);
356
+ }
357
+ })
358
+ .catch((error) => log('platform error', { error }));
359
+ },
360
+ frequency,
361
+ );
362
+ }
363
+
364
+ //
365
+ // Telemetry
366
+ //
367
+
368
+ private _initTelemetry() {
369
+ if (this._secrets.TELEMETRY_API_KEY && this._mode !== 'disabled') {
370
+ this._telemetry = new SegmentTelemetry({
371
+ apiKey: this._secrets.TELEMETRY_API_KEY,
372
+ batchSize: this._telemetryBatchSize,
373
+ getTags: () => this._tags,
374
+ });
375
+ } else {
376
+ log('segment disabled');
377
+ }
378
+ }
379
+
380
+ /**
381
+ * A telemetry event.
382
+ *
383
+ * The default implementation uses Segment.
384
+ */
385
+ event(options: EventOptions) {
386
+ this._telemetry?.event(options);
387
+ }
388
+
389
+ /**
390
+ * A telemetry page view.
391
+ *
392
+ * The default implementation uses Segment.
393
+ */
394
+ page(options: PageOptions) {
395
+ this._telemetry?.page(options);
396
+ }
397
+
398
+ //
399
+ // Error Logs
400
+ //
401
+
402
+ private _initErrorLogs() {
403
+ if (this._secrets.SENTRY_DESTINATION && this._mode !== 'disabled') {
404
+ // TODO(nf): refactor package into this one?
405
+ log.info('Initializing Sentry', {
406
+ dest: this._secrets.SENTRY_DESTINATION,
407
+ options: this._errorReportingOptions,
408
+ });
409
+ sentryInit({
410
+ ...this._errorReportingOptions,
411
+ destination: this._secrets.SENTRY_DESTINATION,
412
+ scrubFilenames: this._mode !== 'full',
413
+ });
414
+ if (this._errorReportingOptions?.tracing) {
415
+ sentryConfigureTracing();
416
+ }
417
+ // TODO(nf): set platform at instantiation? needed for node.
418
+ if (this._errorLogProcessor) {
419
+ enableSentryLogProcessor();
420
+ }
421
+ // TODO(nf): is this different than passing as properties in options?
422
+ this._tags.forEach((v, k) => {
423
+ if (ERROR_TAGS.includes(k)) {
424
+ sentrySetTag(k, v);
425
+ }
426
+ });
427
+ } else {
428
+ log('sentry disabled');
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Manually capture an exception.
434
+ *
435
+ * The default implementation uses Sentry.
436
+ */
437
+ captureException(err: any) {
438
+ if (this.enabled) {
439
+ sentryCaptureException(err);
440
+ }
441
+ }
442
+ }
@@ -0,0 +1,67 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import snippet from '@segment/snippet';
6
+
7
+ import { log } from '@dxos/log';
8
+
9
+ import type { EventOptions, SegmentTelemetryOptions, PageOptions } from './types';
10
+ import { captureException } from '../sentry';
11
+
12
+ export class SegmentTelemetry {
13
+ private _getTags: () => Map<string, string>;
14
+
15
+ constructor({ apiKey, batchSize, getTags }: SegmentTelemetryOptions) {
16
+ this._getTags = getTags;
17
+ try {
18
+ const contents = snippet.min({
19
+ apiKey,
20
+ page: false,
21
+ });
22
+
23
+ const script = document.createElement('script');
24
+ script.innerHTML = contents;
25
+ document.body.append(script);
26
+ } catch (err) {
27
+ log.catch('Failed to initialize telemetry', err);
28
+ }
29
+ }
30
+
31
+ page({ identityId: userId, ...options }: PageOptions = {}) {
32
+ try {
33
+ (window as any).analytics?.page({
34
+ context: Object.fromEntries(this._getTags().entries()),
35
+ ...options,
36
+ userId,
37
+ });
38
+ } catch (err) {
39
+ log.catch('Failed to track page', err);
40
+ }
41
+ }
42
+
43
+ event({ identityId: userId, name: event, ...options }: EventOptions) {
44
+ try {
45
+ options.properties = {
46
+ ...options.properties,
47
+ ...Object.fromEntries(this._getTags().entries()),
48
+ };
49
+ (window as any).analytics?.track({
50
+ ...options,
51
+ event,
52
+ });
53
+ } catch (err) {
54
+ log.catch('Failed to track event', err);
55
+ }
56
+ }
57
+
58
+ async flush() {
59
+ try {
60
+ await (window as any).analytics?.flush((err: any) => {
61
+ captureException(err);
62
+ });
63
+ } catch (err) {
64
+ log.catch('Failed to flush telemetry', err);
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export * from './node';
6
+ export * from './types';
@@ -0,0 +1,91 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import Analytics from 'analytics-node';
6
+
7
+ import { invariant } from '@dxos/invariant';
8
+ import { log } from '@dxos/log';
9
+
10
+ import type { EventOptions, SegmentTelemetryOptions, PageOptions } from './types';
11
+ import { captureException } from '../sentry';
12
+
13
+ export class SegmentTelemetry {
14
+ private _analytics?: Analytics;
15
+ private _getTags: () => Map<string, string>;
16
+
17
+ constructor({ apiKey, batchSize, getTags }: SegmentTelemetryOptions) {
18
+ this._getTags = getTags;
19
+ try {
20
+ invariant(apiKey, 'Key required to send telemetry');
21
+
22
+ this._analytics = new Analytics(apiKey, {
23
+ flushAt: batchSize,
24
+ enable: true,
25
+ });
26
+ } catch (err) {
27
+ log.catch('Failed to initialize telemetry', err);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Track a page view.
33
+ */
34
+ page({ installationId: anonymousId, identityId: userId, ...options }: PageOptions = {}) {
35
+ if (!this._analytics) {
36
+ log('Analytics not initialized', { action: 'page' });
37
+ return;
38
+ }
39
+
40
+ try {
41
+ this._analytics.page({
42
+ ...options,
43
+ userId,
44
+ anonymousId: anonymousId!,
45
+ });
46
+ } catch (err) {
47
+ log.catch('Failed to track page', err);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Track an event.
53
+ */
54
+ event({ installationId: anonymousId, identityId: userId, name: event, ...options }: EventOptions) {
55
+ log('sending event to telemetry', { event, options, tags: Object.fromEntries(this._getTags().entries()) });
56
+ if (!this._analytics) {
57
+ log('Analytics not initialized', { action: 'event' });
58
+ return;
59
+ }
60
+
61
+ try {
62
+ this._analytics.track({
63
+ ...options,
64
+ context: Object.fromEntries(this._getTags().entries()),
65
+ userId,
66
+ anonymousId: anonymousId!,
67
+ event,
68
+ });
69
+ } catch (err) {
70
+ log.catch('Failed to track event', err);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Flush the event queue.
76
+ */
77
+ async flush() {
78
+ if (!this._analytics) {
79
+ log('Analytics not initialized', { action: 'flush' });
80
+ return;
81
+ }
82
+
83
+ try {
84
+ await this._analytics.flush((err) => {
85
+ captureException(err);
86
+ });
87
+ } catch (err) {
88
+ log.catch('Failed to flush', err);
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,27 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export type SegmentTelemetryOptions = {
6
+ apiKey?: string;
7
+ batchSize?: number;
8
+ enable?: boolean;
9
+ getTags: () => Map<string, string>;
10
+ };
11
+
12
+ type CommonOptions = {
13
+ installationId?: string;
14
+ identityId?: string;
15
+ properties?: object;
16
+ timestamp?: Date;
17
+ };
18
+
19
+ export type PageOptions = CommonOptions & {
20
+ installationId?: string;
21
+ name?: string;
22
+ category?: string;
23
+ };
24
+
25
+ export type EventOptions = CommonOptions & {
26
+ name: string;
27
+ };