@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.
- package/dist/lib/browser/chunk-JV7HV5M3.mjs +147 -0
- package/dist/lib/browser/chunk-JV7HV5M3.mjs.map +7 -0
- package/dist/lib/browser/{chunk-RWIBODSU.mjs → chunk-LCUEUKOU.mjs} +155 -107
- package/dist/lib/browser/chunk-LCUEUKOU.mjs.map +7 -0
- package/dist/lib/browser/{chunk-3LKCQYWD.mjs → chunk-WTIK6YHB.mjs} +2 -4
- package/dist/lib/browser/{chunk-3LKCQYWD.mjs.map → chunk-WTIK6YHB.mjs.map} +3 -3
- package/dist/lib/browser/index.mjs +6 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/observability-ZJBNR5EC.mjs +10 -0
- package/dist/lib/browser/segment/index.mjs +7 -81
- package/dist/lib/browser/segment/index.mjs.map +4 -4
- package/dist/lib/browser/sentry/index.mjs +1 -1
- package/dist/lib/browser/{sentry-log-processor-2KNHVHXL.mjs → sentry-log-processor-DVY46HFR.mjs} +2 -2
- package/dist/lib/browser/{sentry-log-processor-2KNHVHXL.mjs.map → sentry-log-processor-DVY46HFR.mjs.map} +2 -2
- package/dist/lib/node/{chunk-X2D2I2IE.cjs → chunk-TZZPITFA.cjs} +156 -109
- package/dist/lib/node/chunk-TZZPITFA.cjs.map +7 -0
- package/dist/lib/node/{chunk-HKBUNAUG.cjs → chunk-VJYGUKWI.cjs} +6 -6
- package/dist/lib/node/{chunk-HKBUNAUG.cjs.map → chunk-VJYGUKWI.cjs.map} +3 -3
- package/dist/lib/node/chunk-XIE7NKDF.cjs +213 -0
- package/dist/lib/node/chunk-XIE7NKDF.cjs.map +7 -0
- package/dist/lib/node/index.cjs +20 -15
- package/dist/lib/node/index.cjs.map +2 -2
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/{observability-MM62IY7P.cjs → observability-LQYGR6H5.cjs} +9 -6
- package/dist/lib/node/observability-LQYGR6H5.cjs.map +7 -0
- package/dist/lib/node/segment/index.cjs +6 -136
- package/dist/lib/node/segment/index.cjs.map +4 -4
- package/dist/lib/node/sentry/index.cjs +10 -10
- package/dist/lib/node/sentry/index.cjs.map +1 -1
- package/dist/lib/node/{sentry-log-processor-SJZRJ7IS.cjs → sentry-log-processor-XWB4ECU6.cjs} +8 -8
- package/dist/lib/node/{sentry-log-processor-SJZRJ7IS.cjs.map → sentry-log-processor-XWB4ECU6.cjs.map} +2 -2
- package/dist/lib/node-esm/{chunk-CMDADTM3.mjs → chunk-ANXHKE36.mjs} +3 -3
- package/dist/lib/node-esm/{chunk-CMDADTM3.mjs.map → chunk-ANXHKE36.mjs.map} +3 -3
- package/dist/lib/node-esm/{chunk-S7JHPCWW.mjs → chunk-HMGFYMJF.mjs} +154 -107
- package/dist/lib/node-esm/chunk-HMGFYMJF.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-IKCDMDT3.mjs +202 -0
- package/dist/lib/node-esm/chunk-IKCDMDT3.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +6 -1
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/observability-KDS7YGCQ.mjs +11 -0
- package/dist/lib/node-esm/segment/index.mjs +7 -139
- package/dist/lib/node-esm/segment/index.mjs.map +4 -4
- package/dist/lib/node-esm/sentry/index.mjs +1 -1
- package/dist/lib/node-esm/{sentry-log-processor-BVINGAOV.mjs → sentry-log-processor-FO6MIWBC.mjs} +2 -2
- package/dist/lib/node-esm/{sentry-log-processor-BVINGAOV.mjs.map → sentry-log-processor-FO6MIWBC.mjs.map} +2 -2
- package/dist/types/src/helpers/browser-observability.d.ts.map +1 -1
- package/dist/types/src/helpers/browser-observability.js +15 -14
- package/dist/types/src/helpers/browser-observability.js.map +1 -1
- package/dist/types/src/helpers/common.d.ts +3 -1
- package/dist/types/src/helpers/common.d.ts.map +1 -1
- package/dist/types/src/helpers/common.js +4 -0
- package/dist/types/src/helpers/common.js.map +1 -1
- package/dist/types/src/helpers/node-observability.d.ts.map +1 -1
- package/dist/types/src/helpers/node-observability.js +2 -2
- package/dist/types/src/helpers/node-observability.js.map +1 -1
- package/dist/types/src/helpers/setup-telemetry-listeners.d.ts +1 -1
- package/dist/types/src/helpers/setup-telemetry-listeners.d.ts.map +1 -1
- package/dist/types/src/helpers/setup-telemetry-listeners.js +38 -36
- package/dist/types/src/helpers/setup-telemetry-listeners.js.map +1 -1
- package/dist/types/src/observability.d.ts +30 -25
- package/dist/types/src/observability.d.ts.map +1 -1
- package/dist/types/src/observability.js +61 -39
- package/dist/types/src/observability.js.map +1 -1
- package/dist/types/src/segment/base.d.ts +15 -0
- package/dist/types/src/segment/base.d.ts.map +1 -0
- package/dist/types/src/segment/base.js +50 -0
- package/dist/types/src/segment/base.js.map +1 -0
- package/dist/types/src/segment/browser.d.ts +10 -5
- package/dist/types/src/segment/browser.d.ts.map +1 -1
- package/dist/types/src/segment/browser.js +27 -25
- package/dist/types/src/segment/browser.js.map +1 -1
- package/dist/types/src/segment/node.d.ts +10 -14
- package/dist/types/src/segment/node.d.ts.map +1 -1
- package/dist/types/src/segment/node.js +26 -29
- package/dist/types/src/segment/node.js.map +1 -1
- package/dist/types/src/segment/types.d.ts +39 -11
- package/dist/types/src/segment/types.d.ts.map +1 -1
- package/dist/types/src/segment/types.js +14 -0
- package/dist/types/src/segment/types.js.map +1 -1
- package/dist/types/src/sentry/browser.d.ts +1 -1
- package/dist/types/src/sentry/browser.d.ts.map +1 -1
- package/dist/types/src/sentry/browser.js +2 -2
- package/dist/types/src/sentry/browser.js.map +1 -1
- package/dist/types/src/sentry/node.d.ts +1 -1
- package/dist/types/src/sentry/node.d.ts.map +1 -1
- package/dist/types/src/sentry/node.js +2 -2
- package/dist/types/src/sentry/node.js.map +1 -1
- package/dist/types/src/sentry/sentry-log-processor.d.ts.map +1 -1
- package/dist/types/src/sentry/sentry-log-processor.js.map +1 -1
- package/package.json +15 -15
- package/src/helpers/browser-observability.ts +13 -17
- package/src/helpers/common.ts +15 -2
- package/src/helpers/node-observability.ts +3 -2
- package/src/helpers/setup-telemetry-listeners.ts +39 -41
- package/src/observability.ts +101 -64
- package/src/segment/base.ts +69 -0
- package/src/segment/browser.ts +28 -29
- package/src/segment/node.ts +30 -33
- package/src/segment/types.ts +39 -9
- package/src/sentry/browser.ts +2 -2
- package/src/sentry/node.ts +2 -2
- package/src/sentry/sentry-log-processor.ts +3 -0
- package/dist/lib/browser/chunk-RWIBODSU.mjs.map +0 -7
- package/dist/lib/browser/observability-VEOITBLM.mjs +0 -7
- package/dist/lib/node/chunk-X2D2I2IE.cjs.map +0 -7
- package/dist/lib/node/observability-MM62IY7P.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-S7JHPCWW.mjs.map +0 -7
- package/dist/lib/node-esm/observability-OLS7SWZT.mjs +0 -8
- /package/dist/lib/browser/{observability-VEOITBLM.mjs.map → observability-ZJBNR5EC.mjs.map} +0 -0
- /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.
|
|
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 {
|
|
8
|
-
import type
|
|
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.
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
55
|
-
setTimeout(() =>
|
|
56
|
-
observability.
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
const duration = now.getTime() - lastFocusEvent.getTime();
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
observability.track({
|
|
56
|
+
...getTelemetryIdentity(client),
|
|
57
|
+
action: 'window.blur',
|
|
59
58
|
properties: {
|
|
60
|
-
|
|
61
|
-
timeSpent,
|
|
59
|
+
duration,
|
|
62
60
|
},
|
|
63
|
-
})
|
|
64
|
-
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
65
64
|
lastFocusEvent = now;
|
|
66
|
-
totalTime = totalTime +
|
|
65
|
+
totalTime = totalTime + duration;
|
|
67
66
|
};
|
|
68
67
|
|
|
69
68
|
const unloadCallback = () => {
|
|
70
|
-
setTimeout(() =>
|
|
71
|
-
observability.
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
observability.track({
|
|
71
|
+
...getTelemetryIdentity(client),
|
|
72
|
+
action: 'page.unload',
|
|
74
73
|
properties: {
|
|
75
|
-
|
|
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.
|
|
85
|
-
|
|
86
|
-
|
|
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);
|
package/src/observability.ts
CHANGED
|
@@ -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,
|
|
16
|
+
import { getTelemetryIdentity, type IPData, mapSpaces } from './helpers';
|
|
17
17
|
import { type OtelLogs, type OtelMetrics, type OtelTraces } from './otel';
|
|
18
|
-
import { type SegmentTelemetry, type
|
|
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
|
-
|
|
46
|
-
|
|
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?: (
|
|
79
|
-
private
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
391
|
-
|
|
392
|
-
|
|
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('
|
|
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
|
-
*
|
|
492
|
-
*
|
|
528
|
+
* Submit telemetry page view.
|
|
493
529
|
* The default implementation uses Segment.
|
|
494
530
|
*/
|
|
495
|
-
|
|
496
|
-
this._telemetry?.
|
|
531
|
+
page(options: PageOptions) {
|
|
532
|
+
this._telemetry?.page(options);
|
|
497
533
|
}
|
|
498
534
|
|
|
499
535
|
/**
|
|
500
|
-
*
|
|
501
|
-
*
|
|
536
|
+
* Submit telemetry user action.
|
|
502
537
|
* The default implementation uses Segment.
|
|
503
538
|
*/
|
|
504
|
-
|
|
505
|
-
this._telemetry?.
|
|
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):
|
|
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):
|
|
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):
|
|
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(
|
|
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?.(
|
|
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
|
+
}
|
package/src/segment/browser.ts
CHANGED
|
@@ -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
|
|
10
|
+
import { AbstractSegmentTelemetry } from './base';
|
|
11
|
+
import type { TrackOptions, SegmentTelemetryOptions, PageOptions } from './types';
|
|
10
12
|
import { captureException } from '../sentry';
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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('
|
|
27
|
+
log.catch('failed to initialize telemetry', err);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
identify(options: IdentifyParams) {
|
|
32
32
|
try {
|
|
33
|
-
(window as any).analytics?.
|
|
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('
|
|
35
|
+
log.catch('failed to identify', err);
|
|
40
36
|
}
|
|
41
37
|
}
|
|
42
38
|
|
|
43
|
-
|
|
39
|
+
page(options: PageOptions) {
|
|
44
40
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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('
|
|
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('
|
|
63
|
+
log.catch('failed to flush telemetry', err);
|
|
65
64
|
}
|
|
66
65
|
}
|
|
67
66
|
|