@dxos/observability 0.8.1-main.ba2dec9 → 0.8.1-staging.31c3ee1

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 (110) hide show
  1. package/dist/lib/browser/chunk-JV7HV5M3.mjs +147 -0
  2. package/dist/lib/browser/chunk-JV7HV5M3.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-RWIBODSU.mjs → chunk-LCUEUKOU.mjs} +155 -107
  4. package/dist/lib/browser/chunk-LCUEUKOU.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-3LKCQYWD.mjs → chunk-WTIK6YHB.mjs} +2 -4
  6. package/dist/lib/browser/{chunk-3LKCQYWD.mjs.map → chunk-WTIK6YHB.mjs.map} +3 -3
  7. package/dist/lib/browser/index.mjs +6 -1
  8. package/dist/lib/browser/meta.json +1 -1
  9. package/dist/lib/browser/observability-ZJBNR5EC.mjs +10 -0
  10. package/dist/lib/browser/segment/index.mjs +7 -81
  11. package/dist/lib/browser/segment/index.mjs.map +4 -4
  12. package/dist/lib/browser/sentry/index.mjs +1 -1
  13. package/dist/lib/browser/{sentry-log-processor-2KNHVHXL.mjs → sentry-log-processor-DVY46HFR.mjs} +2 -2
  14. package/dist/lib/browser/{sentry-log-processor-2KNHVHXL.mjs.map → sentry-log-processor-DVY46HFR.mjs.map} +2 -2
  15. package/dist/lib/node/{chunk-X2D2I2IE.cjs → chunk-TZZPITFA.cjs} +156 -109
  16. package/dist/lib/node/chunk-TZZPITFA.cjs.map +7 -0
  17. package/dist/lib/node/{chunk-HKBUNAUG.cjs → chunk-VJYGUKWI.cjs} +6 -6
  18. package/dist/lib/node/{chunk-HKBUNAUG.cjs.map → chunk-VJYGUKWI.cjs.map} +3 -3
  19. package/dist/lib/node/chunk-XIE7NKDF.cjs +213 -0
  20. package/dist/lib/node/chunk-XIE7NKDF.cjs.map +7 -0
  21. package/dist/lib/node/index.cjs +20 -15
  22. package/dist/lib/node/index.cjs.map +2 -2
  23. package/dist/lib/node/meta.json +1 -1
  24. package/dist/lib/node/{observability-MM62IY7P.cjs → observability-LQYGR6H5.cjs} +9 -6
  25. package/dist/lib/node/observability-LQYGR6H5.cjs.map +7 -0
  26. package/dist/lib/node/segment/index.cjs +6 -136
  27. package/dist/lib/node/segment/index.cjs.map +4 -4
  28. package/dist/lib/node/sentry/index.cjs +10 -10
  29. package/dist/lib/node/sentry/index.cjs.map +1 -1
  30. package/dist/lib/node/{sentry-log-processor-SJZRJ7IS.cjs → sentry-log-processor-XWB4ECU6.cjs} +8 -8
  31. package/dist/lib/node/{sentry-log-processor-SJZRJ7IS.cjs.map → sentry-log-processor-XWB4ECU6.cjs.map} +2 -2
  32. package/dist/lib/node-esm/{chunk-CMDADTM3.mjs → chunk-ANXHKE36.mjs} +3 -3
  33. package/dist/lib/node-esm/{chunk-CMDADTM3.mjs.map → chunk-ANXHKE36.mjs.map} +3 -3
  34. package/dist/lib/node-esm/{chunk-S7JHPCWW.mjs → chunk-HMGFYMJF.mjs} +154 -107
  35. package/dist/lib/node-esm/chunk-HMGFYMJF.mjs.map +7 -0
  36. package/dist/lib/node-esm/chunk-IKCDMDT3.mjs +202 -0
  37. package/dist/lib/node-esm/chunk-IKCDMDT3.mjs.map +7 -0
  38. package/dist/lib/node-esm/index.mjs +6 -1
  39. package/dist/lib/node-esm/meta.json +1 -1
  40. package/dist/lib/node-esm/observability-KDS7YGCQ.mjs +11 -0
  41. package/dist/lib/node-esm/segment/index.mjs +7 -139
  42. package/dist/lib/node-esm/segment/index.mjs.map +4 -4
  43. package/dist/lib/node-esm/sentry/index.mjs +1 -1
  44. package/dist/lib/node-esm/{sentry-log-processor-BVINGAOV.mjs → sentry-log-processor-FO6MIWBC.mjs} +2 -2
  45. package/dist/lib/node-esm/{sentry-log-processor-BVINGAOV.mjs.map → sentry-log-processor-FO6MIWBC.mjs.map} +2 -2
  46. package/dist/types/src/helpers/browser-observability.d.ts.map +1 -1
  47. package/dist/types/src/helpers/browser-observability.js +15 -14
  48. package/dist/types/src/helpers/browser-observability.js.map +1 -1
  49. package/dist/types/src/helpers/common.d.ts +3 -1
  50. package/dist/types/src/helpers/common.d.ts.map +1 -1
  51. package/dist/types/src/helpers/common.js +4 -0
  52. package/dist/types/src/helpers/common.js.map +1 -1
  53. package/dist/types/src/helpers/node-observability.d.ts.map +1 -1
  54. package/dist/types/src/helpers/node-observability.js +2 -2
  55. package/dist/types/src/helpers/node-observability.js.map +1 -1
  56. package/dist/types/src/helpers/setup-telemetry-listeners.d.ts +1 -1
  57. package/dist/types/src/helpers/setup-telemetry-listeners.d.ts.map +1 -1
  58. package/dist/types/src/helpers/setup-telemetry-listeners.js +38 -36
  59. package/dist/types/src/helpers/setup-telemetry-listeners.js.map +1 -1
  60. package/dist/types/src/observability.d.ts +30 -25
  61. package/dist/types/src/observability.d.ts.map +1 -1
  62. package/dist/types/src/observability.js +61 -39
  63. package/dist/types/src/observability.js.map +1 -1
  64. package/dist/types/src/segment/base.d.ts +15 -0
  65. package/dist/types/src/segment/base.d.ts.map +1 -0
  66. package/dist/types/src/segment/base.js +50 -0
  67. package/dist/types/src/segment/base.js.map +1 -0
  68. package/dist/types/src/segment/browser.d.ts +10 -5
  69. package/dist/types/src/segment/browser.d.ts.map +1 -1
  70. package/dist/types/src/segment/browser.js +27 -25
  71. package/dist/types/src/segment/browser.js.map +1 -1
  72. package/dist/types/src/segment/node.d.ts +10 -14
  73. package/dist/types/src/segment/node.d.ts.map +1 -1
  74. package/dist/types/src/segment/node.js +26 -29
  75. package/dist/types/src/segment/node.js.map +1 -1
  76. package/dist/types/src/segment/types.d.ts +39 -11
  77. package/dist/types/src/segment/types.d.ts.map +1 -1
  78. package/dist/types/src/segment/types.js +14 -0
  79. package/dist/types/src/segment/types.js.map +1 -1
  80. package/dist/types/src/sentry/browser.d.ts +1 -1
  81. package/dist/types/src/sentry/browser.d.ts.map +1 -1
  82. package/dist/types/src/sentry/browser.js +2 -2
  83. package/dist/types/src/sentry/browser.js.map +1 -1
  84. package/dist/types/src/sentry/node.d.ts +1 -1
  85. package/dist/types/src/sentry/node.d.ts.map +1 -1
  86. package/dist/types/src/sentry/node.js +2 -2
  87. package/dist/types/src/sentry/node.js.map +1 -1
  88. package/dist/types/src/sentry/sentry-log-processor.d.ts.map +1 -1
  89. package/dist/types/src/sentry/sentry-log-processor.js.map +1 -1
  90. package/package.json +15 -15
  91. package/src/helpers/browser-observability.ts +13 -17
  92. package/src/helpers/common.ts +15 -2
  93. package/src/helpers/node-observability.ts +3 -2
  94. package/src/helpers/setup-telemetry-listeners.ts +39 -41
  95. package/src/observability.ts +101 -64
  96. package/src/segment/base.ts +69 -0
  97. package/src/segment/browser.ts +28 -29
  98. package/src/segment/node.ts +30 -33
  99. package/src/segment/types.ts +39 -9
  100. package/src/sentry/browser.ts +2 -2
  101. package/src/sentry/node.ts +2 -2
  102. package/src/sentry/sentry-log-processor.ts +3 -0
  103. package/dist/lib/browser/chunk-RWIBODSU.mjs.map +0 -7
  104. package/dist/lib/browser/observability-VEOITBLM.mjs +0 -7
  105. package/dist/lib/node/chunk-X2D2I2IE.cjs.map +0 -7
  106. package/dist/lib/node/observability-MM62IY7P.cjs.map +0 -7
  107. package/dist/lib/node-esm/chunk-S7JHPCWW.mjs.map +0 -7
  108. package/dist/lib/node-esm/observability-OLS7SWZT.mjs +0 -8
  109. /package/dist/lib/browser/{observability-VEOITBLM.mjs.map → observability-ZJBNR5EC.mjs.map} +0 -0
  110. /package/dist/lib/node-esm/{observability-OLS7SWZT.mjs.map → observability-KDS7YGCQ.mjs.map} +0 -0
@@ -114,11 +114,11 @@ export const initializeNodeObservability = async ({
114
114
  const environment = process.env.DX_ENVIRONMENT ?? 'unknown';
115
115
 
116
116
  const observability = new Observability({
117
+ mode,
117
118
  namespace,
118
119
  release,
119
120
  environment,
120
121
  group,
121
- mode,
122
122
  errorLog: {
123
123
  sentryInitOptions: {
124
124
  environment,
@@ -136,9 +136,10 @@ export const initializeNodeObservability = async ({
136
136
  try {
137
137
  const res = await fetch(`https://api.ipdata.co/?api-key=${IPDATA_API_KEY}`);
138
138
  const ipData = await res.json();
139
- ipData && observability.addIPDataTelemetryTags(ipData);
139
+ ipData && observability.setIPDataTelemetryTags(ipData);
140
140
  } catch (err) {
141
141
  observability?.captureException(err);
142
142
  }
143
+
143
144
  return observability;
144
145
  };
@@ -4,8 +4,8 @@
4
4
 
5
5
  import type { Client } from '@dxos/client';
6
6
 
7
- import { getTelemetryIdentifier } from './common';
8
- import type { Observability } from '../observability';
7
+ import { getTelemetryIdentity } from './common';
8
+ import { type Observability } from '../observability';
9
9
 
10
10
  let lastFocusEvent = new Date();
11
11
  let totalTime = 0;
@@ -17,12 +17,11 @@ export const setupTelemetryListeners = (namespace: string, client: Client, obser
17
17
  return;
18
18
  }
19
19
 
20
- setTimeout(() =>
21
- observability.event({
22
- did: getTelemetryIdentifier(client),
23
- name: `${namespace}.window.click`,
20
+ setTimeout(() => {
21
+ observability.track({
22
+ ...getTelemetryIdentity(client),
23
+ action: 'window.click',
24
24
  properties: {
25
- href: window.location.href,
26
25
  id: (event.target as HTMLElement)?.id,
27
26
  path: (event.composedPath() as HTMLElement[])
28
27
  .filter((el) => Boolean(el.tagName))
@@ -30,68 +29,67 @@ export const setupTelemetryListeners = (namespace: string, client: Client, obser
30
29
  .reverse()
31
30
  .join('>'),
32
31
  },
33
- }),
34
- );
32
+ });
33
+ });
35
34
  };
36
35
 
37
36
  const focusCallback = () => {
38
37
  const now = new Date();
39
- setTimeout(() =>
40
- observability.event({
41
- did: getTelemetryIdentifier(client),
42
- name: `${namespace}.window.focus`,
38
+ setTimeout(() => {
39
+ observability.track({
40
+ ...getTelemetryIdentity(client),
41
+ action: 'window.focus',
43
42
  properties: {
44
- href: window.location.href,
45
43
  timeAway: now.getTime() - lastFocusEvent.getTime(),
46
44
  },
47
- }),
48
- );
45
+ });
46
+ });
47
+
49
48
  lastFocusEvent = now;
50
49
  };
51
50
 
52
51
  const blurCallback = () => {
53
52
  const now = new Date();
54
- const timeSpent = now.getTime() - lastFocusEvent.getTime();
55
- setTimeout(() =>
56
- observability.event({
57
- did: getTelemetryIdentifier(client),
58
- name: `${namespace}.window.blur`,
53
+ const duration = now.getTime() - lastFocusEvent.getTime();
54
+ setTimeout(() => {
55
+ observability.track({
56
+ ...getTelemetryIdentity(client),
57
+ action: 'window.blur',
59
58
  properties: {
60
- href: window.location.href,
61
- timeSpent,
59
+ duration,
62
60
  },
63
- }),
64
- );
61
+ });
62
+ });
63
+
65
64
  lastFocusEvent = now;
66
- totalTime = totalTime + timeSpent;
65
+ totalTime = totalTime + duration;
67
66
  };
68
67
 
69
68
  const unloadCallback = () => {
70
- setTimeout(() =>
71
- observability.event({
72
- did: getTelemetryIdentifier(client),
73
- name: `${namespace}.page.unload`,
69
+ setTimeout(() => {
70
+ observability.track({
71
+ ...getTelemetryIdentity(client),
72
+ action: 'page.unload',
74
73
  properties: {
75
- href: window.location.href,
76
- timeSpent: totalTime,
74
+ duration: totalTime,
77
75
  },
78
- }),
79
- );
76
+ });
77
+ });
80
78
  };
81
79
 
82
80
  const errorCallback = (event: ErrorEvent) => {
83
- setTimeout(() =>
84
- observability.event({
85
- did: getTelemetryIdentifier(client),
86
- name: `${namespace}.window.error`,
81
+ setTimeout(() => {
82
+ observability.track({
83
+ ...getTelemetryIdentity(client),
84
+ action: 'window.error',
87
85
  properties: {
88
- href: window.location.href,
89
86
  message: event.message,
90
87
  filename: event.filename,
91
88
  stack: (event.error as Error)?.stack,
89
+ cause: (event.error as Error)?.cause,
92
90
  },
93
- }),
94
- );
91
+ });
92
+ });
95
93
  };
96
94
 
97
95
  window.addEventListener('click', clickCallback, true);
@@ -13,9 +13,9 @@ import { DeviceKind, type NetworkStatus, Platform } from '@dxos/protocols/proto/
13
13
  import { isNode } from '@dxos/util';
14
14
 
15
15
  import buildSecrets from './cli-observability-secrets.json';
16
- import { type IPData, getTelemetryIdentifier, mapSpaces } from './helpers';
16
+ import { getTelemetryIdentity, type IPData, mapSpaces } from './helpers';
17
17
  import { type OtelLogs, type OtelMetrics, type OtelTraces } from './otel';
18
- import { type SegmentTelemetry, type EventOptions, type PageOptions } from './segment';
18
+ import { type SegmentTelemetry, type TrackOptions, type PageOptions, TelemetryEvent } from './segment';
19
19
  import { type InitOptions, type captureException as SentryCaptureException } from './sentry';
20
20
  import { type SentryLogProcessor } from './sentry/sentry-log-processor';
21
21
 
@@ -35,20 +35,28 @@ export type ObservabilitySecrets = {
35
35
  OTEL_AUTHORIZATION: string | null;
36
36
  };
37
37
 
38
- export type Mode = 'basic' | 'full' | 'disabled';
39
38
  export type TagScope = 'errors' | 'telemetry' | 'metrics' | 'all';
40
39
 
40
+ /** Gathering mode. */
41
+ export type Mode = 'basic' | 'full' | 'disabled';
42
+
41
43
  export type ObservabilityOptions = {
42
- /// The webapp (e.g. 'composer.dxos.org'), 'cli', or 'agent'.
43
- namespace: string;
44
44
  mode: Mode;
45
- // TODO(nf): make platform a required extension?
46
- // platform: Platform;
47
- release?: string;
45
+
46
+ /** Environment. */
48
47
  environment?: string;
48
+
49
+ /** Application namespace. */
50
+ namespace: string;
51
+
52
+ /** Application release. */
53
+ release?: string;
54
+
55
+ /** User group. */
56
+ group?: string;
57
+
49
58
  config?: Config;
50
59
  secrets?: Record<string, string>;
51
- group?: string;
52
60
 
53
61
  telemetry?: {
54
62
  batchSize?: number;
@@ -62,8 +70,34 @@ export type ObservabilityOptions = {
62
70
  /*
63
71
  * Observability provides a common interface for error logging, metrics, and telemetry.
64
72
  * It currently provides these capabilities using Sentry, OpenTelemetry, and Segment.
73
+ *
74
+ * Segment:
75
+ * https://app.segment.com/dxos/sources/composer-app/debugger
76
+ * https://app.segment.com/dxos/sources/composer-app/settings/keys
77
+ *
78
+ * NOTE:
79
+ * - Segment maintains a set of admin creates Source (e.g., "composer-app").
80
+ * - Each source has at least one API_KEY, which is used by the client.
81
+ *
82
+ * Testing:
83
+ * https://app.segment.com/dxos/sources/composer-app/settings/keys
84
+ * - DX_TELEMETRY_API_KEY
85
+ * - DX_SENTRY_DESTINATION
86
+ *
87
+ * Sentry:
88
+ * https://sentry.io/organizations/dxos/issues
89
+ *
90
+ * OpenTelemetry:
91
+ * https://dxosorg.grafana.net/explore
65
92
  */
66
93
  export class Observability {
94
+ private _mode: Mode;
95
+ private readonly _namespace: string;
96
+ private readonly _config?: Config;
97
+ private readonly _group?: string;
98
+ private readonly _secrets: ObservabilitySecrets;
99
+ private readonly _tags = new Map<string, { value: string; scope: TagScope }>();
100
+
67
101
  // TODO(wittjosiah): Generic metrics interface.
68
102
  private _otelMetrics?: OtelMetrics;
69
103
  private _otelTraces?: OtelTraces;
@@ -75,47 +109,39 @@ export class Observability {
75
109
  private _otelLogs?: OtelLogs;
76
110
  private _errorReportingOptions?: InitOptions;
77
111
  private _captureException?: typeof SentryCaptureException;
78
- private _captureUserFeedback?: (name: string, email: string, message: string) => Promise<void>;
79
- private _setTag?: (key: string, value: string) => void;
112
+ private _captureUserFeedback?: (message: string) => Promise<void>;
113
+ private _lastNetworkStatus?: NetworkStatus;
80
114
 
81
- private _secrets: ObservabilitySecrets;
82
- private _namespace: string;
83
- private _mode: Mode;
84
- private _config?: Config;
85
- private _group?: string;
86
- // TODO(nf): accept upstream context?
87
115
  private _ctx = new Context();
88
- private _tags = new Map<string, { value: string; scope: TagScope }>();
89
- private _lastNetworkStatus?: NetworkStatus;
90
116
 
91
117
  // TODO(nf): make platform a required extension?
92
118
  constructor({
119
+ mode,
93
120
  namespace,
94
121
  environment,
95
122
  release,
96
123
  config,
97
- secrets,
98
124
  group,
99
- mode,
125
+ secrets,
100
126
  telemetry,
101
127
  errorLog,
102
128
  }: ObservabilityOptions) {
103
- this._namespace = namespace;
104
129
  this._mode = mode;
130
+ this._namespace = namespace;
105
131
  this._config = config;
106
132
  this._group = group;
107
133
  this._secrets = this._loadSecrets(config, secrets);
134
+
108
135
  this._telemetryBatchSize = telemetry?.batchSize ?? 30;
109
136
  this._errorReportingOptions = errorLog?.sentryInitOptions;
110
137
 
111
- if (this._group) {
112
- this.setTag('group', this._group);
113
- }
114
- this.setTag('namespace', this._namespace);
115
- environment && this.setTag('environment', environment);
116
- release && this.setTag('release', release);
138
+ // Tags.
117
139
  this.setTag('mode', this._mode);
140
+ this.setTag('namespace', this._namespace);
141
+ this.setTag('environment', environment);
142
+ this.setTag('release', release);
118
143
  this.setTag('session', PublicKey.random().toHex());
144
+ this.setTag('group', this._group);
119
145
  }
120
146
 
121
147
  get mode() {
@@ -139,7 +165,6 @@ export class Observability {
139
165
 
140
166
  process.env.DX_ENVIRONMENT && (mergedSecrets.DX_ENVIRONMENT = process.env.DX_ENVIRONMENT);
141
167
  process.env.DX_RELEASE && (mergedSecrets.DX_RELEASE = process.env.DX_RELEASE);
142
- // TODO: prefix these with DX_?
143
168
  process.env.SENTRY_DESTINATION && (mergedSecrets.SENTRY_DESTINATION = process.env.SENTRY_DESTINATION);
144
169
  process.env.TELEMETRY_API_KEY && (mergedSecrets.TELEMETRY_API_KEY = process.env.TELEMETRY_API_KEY);
145
170
  process.env.IPDATA_API_KEY && (mergedSecrets.IPDATA_API_KEY = process.env.IPDATA_API_KEY);
@@ -163,6 +188,7 @@ export class Observability {
163
188
  }
164
189
 
165
190
  async initialize() {
191
+ log('initializing...');
166
192
  await this._initLogs();
167
193
  await this._initMetrics();
168
194
  await this._initTelemetry();
@@ -171,6 +197,7 @@ export class Observability {
171
197
  }
172
198
 
173
199
  async close() {
200
+ log('closing...');
174
201
  const closes: Promise<void>[] = [];
175
202
  this._telemetry && closes.push(this._telemetry.close());
176
203
  this._otelMetrics && closes.push(this._otelMetrics.close());
@@ -178,8 +205,6 @@ export class Observability {
178
205
 
179
206
  await Promise.all(closes);
180
207
  await this._ctx.dispose();
181
-
182
- // TODO(wittjosiah): Remove telemetry, etc. scripts.
183
208
  }
184
209
 
185
210
  setMode(mode: Mode) {
@@ -190,13 +215,23 @@ export class Observability {
190
215
  // Tags
191
216
  //
192
217
 
193
- setTag(key: string, value: string, scope?: TagScope) {
218
+ /** Callback (e.g., to share tags with Sentry.) */
219
+ private _setTag?: (key: string, value: string) => void;
220
+
221
+ /**
222
+ * camelCase keys are converted to snake_case in Segment.
223
+ */
224
+ setTag(key: string, value: string | undefined, scope?: TagScope) {
225
+ if (value === undefined) {
226
+ return;
227
+ }
194
228
  if (this.enabled && (scope === undefined || scope === 'all' || scope === 'errors')) {
195
229
  this._setTag?.(key, value);
196
230
  }
197
231
  if (!scope) {
198
232
  scope = 'all';
199
233
  }
234
+
200
235
  this._tags.set(key, { value, scope });
201
236
  }
202
237
 
@@ -204,14 +239,6 @@ export class Observability {
204
239
  return this._tags.get(key);
205
240
  }
206
241
 
207
- addIPDataTelemetryTags = (ipData: IPData) => {
208
- this.setTag('city', ipData.city, 'telemetry');
209
- this.setTag('region', ipData.region, 'telemetry');
210
- this.setTag('country', ipData.country, 'telemetry');
211
- ipData.latitude && this.setTag('latitude', ipData.latitude.toString(), 'telemetry');
212
- ipData.longitude && this.setTag('longitude', ipData.longitude.toString(), 'telemetry');
213
- };
214
-
215
242
  // TODO(wittjosiah): Improve privacy of telemetry identifiers. See `getTelemetryIdentifier`.
216
243
  async setIdentityTags(clientServices: Partial<ClientServices>) {
217
244
  if (clientServices.IdentityService) {
@@ -220,7 +247,9 @@ export class Observability {
220
247
  log('empty response from identity service', { idqr });
221
248
  return;
222
249
  }
250
+
223
251
  this.setTag('did', idqr.identity.did);
252
+ this._telemetry?.identify({ userId: idqr.identity.did });
224
253
  });
225
254
  }
226
255
 
@@ -230,13 +259,14 @@ export class Observability {
230
259
  log('empty response from device service', { device: dqr });
231
260
  return;
232
261
  }
233
- invariant(dqr, 'empty response from device service');
234
262
 
263
+ invariant(dqr, 'empty response from device service');
235
264
  const thisDevice = dqr.devices.find((device) => device.kind === DeviceKind.CURRENT);
236
265
  if (!thisDevice) {
237
266
  log('no current device', { device: dqr });
238
267
  return;
239
268
  }
269
+
240
270
  this.setTag('deviceKey', thisDevice.deviceKey.truncate());
241
271
  if (thisDevice.profile?.label) {
242
272
  this.setTag('deviceProfile', thisDevice.profile.label);
@@ -245,6 +275,14 @@ export class Observability {
245
275
  }
246
276
  }
247
277
 
278
+ setIPDataTelemetryTags = (ipData: IPData) => {
279
+ this.setTag('city', ipData.city, 'telemetry');
280
+ this.setTag('region', ipData.region, 'telemetry');
281
+ this.setTag('country', ipData.country, 'telemetry');
282
+ ipData.latitude && this.setTag('latitude', ipData.latitude.toString(), 'telemetry');
283
+ ipData.longitude && this.setTag('longitude', ipData.longitude.toString(), 'telemetry');
284
+ };
285
+
248
286
  //
249
287
  // Logs
250
288
  //
@@ -342,7 +380,6 @@ export class Observability {
342
380
  connectionStates.set(conn.state, (connectionStates.get(conn.state) ?? 0) + 1);
343
381
  totalReadBufferSize += conn.readBufferSize ?? 0;
344
382
  totalWriteBufferSize += conn.writeBufferSize ?? 0;
345
-
346
383
  for (const stream of conn.streams ?? []) {
347
384
  totalChannelBufferSize += stream.writeBufferSize ?? 0;
348
385
  }
@@ -387,9 +424,10 @@ export class Observability {
387
424
  updateSpaceTelemetry.on(this._ctx, async () => {
388
425
  log('send space telemetry');
389
426
  for (const data of mapSpaces(spaces, { truncateKeys: true })) {
390
- this.event({
391
- did: getTelemetryIdentifier(client),
392
- name: `${namespace}.space.update`,
427
+ this.track({
428
+ ...getTelemetryIdentity(client),
429
+ event: TelemetryEvent.METRICS,
430
+ action: 'space.update',
393
431
  properties: data,
394
432
  });
395
433
  }
@@ -424,9 +462,8 @@ export class Observability {
424
462
  const platform = await client.services.services.SystemService?.getPlatform();
425
463
  invariant(platform, 'platform is required');
426
464
 
427
- this.setTag('platform_type', Platform.PLATFORM_TYPE[platform.type as number].toLowerCase());
465
+ this.setTag('platformType', Platform.PLATFORM_TYPE[platform.type as number].toLowerCase());
428
466
  if (this._mode === 'full') {
429
- // platform[foo] does not work?
430
467
  if (platform.platform) {
431
468
  this.setTag('platform', platform.platform);
432
469
  }
@@ -488,21 +525,19 @@ export class Observability {
488
525
  }
489
526
 
490
527
  /**
491
- * A telemetry event.
492
- *
528
+ * Submit telemetry page view.
493
529
  * The default implementation uses Segment.
494
530
  */
495
- event(options: EventOptions) {
496
- this._telemetry?.event(options);
531
+ page(options: PageOptions) {
532
+ this._telemetry?.page(options);
497
533
  }
498
534
 
499
535
  /**
500
- * A telemetry page view.
501
- *
536
+ * Submit telemetry user action.
502
537
  * The default implementation uses Segment.
503
538
  */
504
- page(options: PageOptions) {
505
- this._telemetry?.page(options);
539
+ track(options: TrackOptions) {
540
+ this._telemetry?.track(options);
506
541
  }
507
542
 
508
543
  //
@@ -515,10 +550,9 @@ export class Observability {
515
550
  const { SentryLogProcessor } = await import('./sentry/sentry-log-processor');
516
551
  this._captureException = captureException;
517
552
  this._captureUserFeedback = captureUserFeedback;
518
-
519
553
  this._setTag = setTag;
520
554
 
521
- // TODO(nf): refactor package into this one?
555
+ // TODO(nf): Refactor package into this one?
522
556
  log.info('Initializing Sentry', {
523
557
  dest: this._secrets.SENTRY_DESTINATION,
524
558
  options: this._errorReportingOptions,
@@ -530,9 +564,9 @@ export class Observability {
530
564
  scrubFilenames: this._mode !== 'full',
531
565
  onError: (event) => this._sentryLogProcessor!.addLogBreadcrumbsTo(event),
532
566
  });
533
- // TODO(nf): set platform at instantiation? needed for node.
534
567
 
535
- // TODO(nf): is this different than passing as properties in options?
568
+ // TODO(nf): Set platform at instantiation? needed for node.
569
+ // TODO(nf): Is this different than passing as properties in options?
536
570
  this._tags.forEach((v, k) => {
537
571
  if (v.scope === 'all' || v.scope === 'errors') {
538
572
  setTag(k, v.value);
@@ -551,7 +585,7 @@ export class Observability {
551
585
  this._otelTraces && this._otelTraces.start();
552
586
  }
553
587
 
554
- // TODO(nf): refactor init based on providers and their capabilities
588
+ // TODO(nf): Refactor init based on providers and their capabilities.
555
589
  private async _initTraces() {
556
590
  if (this._secrets.OTEL_ENDPOINT && this._secrets.OTEL_AUTHORIZATION && this._mode !== 'disabled') {
557
591
  const { OtelTraces } = await import('./otel');
@@ -574,7 +608,6 @@ export class Observability {
574
608
 
575
609
  /**
576
610
  * Manually capture an exception.
577
- *
578
611
  * The default implementation uses Sentry.
579
612
  */
580
613
  captureException(err: any) {
@@ -585,13 +618,17 @@ export class Observability {
585
618
 
586
619
  /**
587
620
  * Manually capture user feedback.
588
- *
589
621
  * The default implementation uses Sentry.
590
622
  */
591
- captureUserFeedback(name: string, email: string, message: string) {
623
+ captureUserFeedback(message: string) {
624
+ if (!this._secrets.SENTRY_DESTINATION) {
625
+ log.info('Feedback submitted without Sentry destination', { message });
626
+ return;
627
+ }
628
+
592
629
  // TODO(Zan): Should this respect telemetry mode? Sending feedback is explicitly user-initiated.
593
630
  // - Maybe if telemetry is disable we shouldn't enable replay.
594
631
  // - (Check the browser.ts implementation for reference).
595
- void this._captureUserFeedback?.(name, email, message);
632
+ void this._captureUserFeedback?.(message);
596
633
  }
597
634
  }
@@ -0,0 +1,69 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type TrackParams, type PageParams, type IdentifyParams } from '@segment/analytics-node';
6
+
7
+ import { log } from '@dxos/log';
8
+
9
+ import {
10
+ type IdentityOptions,
11
+ type PageOptions,
12
+ type SegmentIdentityOptions,
13
+ type Tags,
14
+ TelemetryEvent,
15
+ type TrackOptions,
16
+ } from './types';
17
+
18
+ /**
19
+ * NOTE: Segment provides a default ID if we don't provide one.
20
+ */
21
+ const getIdentityOptions = ({ did, installationId }: IdentityOptions): SegmentIdentityOptions => {
22
+ if (!did && !installationId) {
23
+ log.warn('No telemetry identifier provided.');
24
+ }
25
+
26
+ return {
27
+ userId: did,
28
+ anonymousId: installationId,
29
+ } as SegmentIdentityOptions;
30
+ };
31
+
32
+ /**
33
+ * Base class for Node and BrowserSegment telemetry.
34
+ */
35
+ export abstract class AbstractSegmentTelemetry {
36
+ constructor(private readonly _getTags: () => Tags) {}
37
+
38
+ abstract identify(options: IdentifyParams): void;
39
+
40
+ abstract page(options: PageOptions): void;
41
+
42
+ abstract track(options: TrackOptions): void;
43
+
44
+ protected createPageProps(options: PageOptions): PageParams {
45
+ const { properties, ...rest } = options;
46
+ return {
47
+ ...getIdentityOptions(options),
48
+ ...rest,
49
+ properties: {
50
+ common: this._getTags(),
51
+ custom: properties,
52
+ },
53
+ };
54
+ }
55
+
56
+ protected createTrackProps(options: TrackOptions): TrackParams {
57
+ const { event, action, properties, ...rest } = options;
58
+ return {
59
+ ...getIdentityOptions(options),
60
+ ...rest,
61
+ event: event ?? TelemetryEvent.ACTION,
62
+ properties: {
63
+ action,
64
+ common: this._getTags(),
65
+ custom: properties,
66
+ },
67
+ };
68
+ }
69
+ }
@@ -2,56 +2,55 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ import { type IdentifyParams } from '@segment/analytics-node';
5
6
  import snippet from '@segment/snippet';
6
7
 
7
8
  import { log } from '@dxos/log';
8
9
 
9
- import type { EventOptions, SegmentTelemetryOptions, PageOptions } from './types';
10
+ import { AbstractSegmentTelemetry } from './base';
11
+ import type { TrackOptions, SegmentTelemetryOptions, PageOptions } from './types';
10
12
  import { captureException } from '../sentry';
11
13
 
12
- export class SegmentTelemetry {
13
- private _getTags: () => { [key: string]: string };
14
-
14
+ /**
15
+ * Browser telemetry.
16
+ */
17
+ // https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/#basic-tracking-methods
18
+ export class SegmentTelemetry extends AbstractSegmentTelemetry {
15
19
  constructor({ apiKey, batchSize, getTags }: SegmentTelemetryOptions) {
16
- this._getTags = getTags;
20
+ super(getTags);
17
21
  try {
18
- const contents = snippet.min({
19
- apiKey,
20
- page: false,
21
- });
22
-
22
+ const contents = snippet.min({ apiKey, page: false });
23
23
  const script = document.createElement('script');
24
24
  script.innerHTML = contents;
25
25
  document.body.append(script);
26
26
  } catch (err) {
27
- log.catch('Failed to initialize telemetry', err);
27
+ log.catch('failed to initialize telemetry', err);
28
28
  }
29
29
  }
30
30
 
31
- page({ did: userId, ...options }: PageOptions = {}) {
31
+ identify(options: IdentifyParams) {
32
32
  try {
33
- (window as any).analytics?.page({
34
- context: this._getTags(),
35
- ...options,
36
- userId,
37
- });
33
+ (window as any).analytics?.identify(options.userId, options.traits);
38
34
  } catch (err) {
39
- log.catch('Failed to track page', err);
35
+ log.catch('failed to identify', err);
40
36
  }
41
37
  }
42
38
 
43
- event({ did: userId, name: event, ...options }: EventOptions) {
39
+ page(options: PageOptions) {
44
40
  try {
45
- options.properties = {
46
- ...options.properties,
47
- ...this._getTags(),
48
- };
49
- (window as any).analytics?.track({
50
- ...options,
51
- event,
52
- });
41
+ const props = this.createPageProps(options);
42
+ (window as any).analytics?.page(props.category, props.name, props.properties);
43
+ } catch (err) {
44
+ log.catch('failed to track page', err);
45
+ }
46
+ }
47
+
48
+ track(options: TrackOptions) {
49
+ try {
50
+ const props = this.createTrackProps(options);
51
+ (window as any).analytics?.track(props.event, props.properties);
53
52
  } catch (err) {
54
- log.catch('Failed to track event', err);
53
+ log.catch('failed to track event', err);
55
54
  }
56
55
  }
57
56
 
@@ -61,7 +60,7 @@ export class SegmentTelemetry {
61
60
  captureException(err);
62
61
  });
63
62
  } catch (err) {
64
- log.catch('Failed to flush telemetry', err);
63
+ log.catch('failed to flush telemetry', err);
65
64
  }
66
65
  }
67
66