workos 0.15.2 → 0.16.0
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/README.md +37 -11
- package/dist/bin.js +1439 -1257
- package/dist/bin.js.map +1 -1
- package/dist/cli.config.d.ts +1 -0
- package/dist/cli.config.js +1 -0
- package/dist/cli.config.js.map +1 -1
- package/dist/commands/api/index.js +7 -2
- package/dist/commands/api/index.js.map +1 -1
- package/dist/commands/api/interactive.js +9 -3
- package/dist/commands/api/interactive.js.map +1 -1
- package/dist/commands/debug.d.ts +2 -1
- package/dist/commands/debug.js +43 -3
- package/dist/commands/debug.js.map +1 -1
- package/dist/commands/dev.js +5 -4
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.js +13 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/emulate.js +2 -2
- package/dist/commands/emulate.js.map +1 -1
- package/dist/commands/env.js +5 -4
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/install-skill.js +4 -3
- package/dist/commands/install-skill.js.map +1 -1
- package/dist/commands/install.js +2 -2
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/login.js +3 -3
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/migrations.d.ts +1 -1
- package/dist/commands/migrations.js +4 -1
- package/dist/commands/migrations.js.map +1 -1
- package/dist/commands/telemetry.d.ts +3 -0
- package/dist/commands/telemetry.js +88 -0
- package/dist/commands/telemetry.js.map +1 -0
- package/dist/commands/uninstall-skill.js +3 -2
- package/dist/commands/uninstall-skill.js.map +1 -1
- package/dist/commands/vault-run.d.ts +13 -0
- package/dist/commands/vault-run.js +194 -0
- package/dist/commands/vault-run.js.map +1 -0
- package/dist/commands/vault.d.ts +3 -2
- package/dist/commands/vault.js +41 -8
- package/dist/commands/vault.js.map +1 -1
- package/dist/lib/api-error-handler.d.ts +15 -3
- package/dist/lib/api-error-handler.js +52 -34
- package/dist/lib/api-error-handler.js.map +1 -1
- package/dist/lib/command-aliases.d.ts +8 -0
- package/dist/lib/command-aliases.js +12 -0
- package/dist/lib/command-aliases.js.map +1 -0
- package/dist/lib/constants.d.ts +0 -1
- package/dist/lib/constants.js +0 -1
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/device-id.d.ts +25 -0
- package/dist/lib/device-id.js +102 -0
- package/dist/lib/device-id.js.map +1 -0
- package/dist/lib/preferences.d.ts +101 -0
- package/dist/lib/preferences.js +198 -0
- package/dist/lib/preferences.js.map +1 -0
- package/dist/lib/run-with-core.js +15 -15
- package/dist/lib/run-with-core.js.map +1 -1
- package/dist/lib/settings.d.ts +6 -0
- package/dist/lib/settings.js +7 -0
- package/dist/lib/settings.js.map +1 -1
- package/dist/lib/telemetry-notice.d.ts +25 -0
- package/dist/lib/telemetry-notice.js +56 -0
- package/dist/lib/telemetry-notice.js.map +1 -0
- package/dist/test/force-insecure-storage.d.ts +1 -0
- package/dist/test/force-insecure-storage.js +9 -0
- package/dist/test/force-insecure-storage.js.map +1 -0
- package/dist/test/setup.d.ts +1 -0
- package/dist/test/setup.js +38 -0
- package/dist/test/setup.js.map +1 -0
- package/dist/utils/analytics.d.ts +41 -0
- package/dist/utils/analytics.js +199 -12
- package/dist/utils/analytics.js.map +1 -1
- package/dist/utils/box.d.ts +29 -1
- package/dist/utils/box.js +92 -4
- package/dist/utils/box.js.map +1 -1
- package/dist/utils/cli-exit.d.ts +15 -0
- package/dist/utils/cli-exit.js +11 -0
- package/dist/utils/cli-exit.js.map +1 -0
- package/dist/utils/cli-symbols.d.ts +1 -1
- package/dist/utils/command-telemetry.d.ts +17 -0
- package/dist/utils/command-telemetry.js +67 -0
- package/dist/utils/command-telemetry.js.map +1 -0
- package/dist/utils/crash-reporter.d.ts +13 -0
- package/dist/utils/crash-reporter.js +91 -0
- package/dist/utils/crash-reporter.js.map +1 -0
- package/dist/utils/debug.d.ts +1 -0
- package/dist/utils/debug.js +4 -1
- package/dist/utils/debug.js.map +1 -1
- package/dist/utils/exit-codes.d.ts +5 -0
- package/dist/utils/exit-codes.js +30 -1
- package/dist/utils/exit-codes.js.map +1 -1
- package/dist/utils/help-json.d.ts +6 -0
- package/dist/utils/help-json.js +87 -10
- package/dist/utils/help-json.js.map +1 -1
- package/dist/utils/output.d.ts +7 -2
- package/dist/utils/output.js +9 -2
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/telemetry-client.d.ts +30 -2
- package/dist/utils/telemetry-client.js +122 -12
- package/dist/utils/telemetry-client.js.map +1 -1
- package/dist/utils/telemetry-store-forward.d.ts +11 -0
- package/dist/utils/telemetry-store-forward.js +94 -0
- package/dist/utils/telemetry-store-forward.js.map +1 -0
- package/dist/utils/telemetry-types.d.ts +58 -9
- package/dist/utils/telemetry-types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { debug, isDebugEnabled } from './debug.js';
|
|
4
|
+
import { getCredentials, isTokenExpired } from '../lib/credentials.js';
|
|
5
|
+
function summarizeEvent(event) {
|
|
6
|
+
switch (event.type) {
|
|
7
|
+
case 'session.start':
|
|
8
|
+
return `session.start(mode=${event.attributes['installer.mode']}, os=${event.attributes['env.os']})`;
|
|
9
|
+
case 'session.end':
|
|
10
|
+
return `session.end(outcome=${event.attributes['installer.outcome']}, duration=${event.attributes['installer.duration_ms']}ms)`;
|
|
11
|
+
case 'step':
|
|
12
|
+
return `step(${event.name}, ${event.durationMs}ms, success=${event.success})`;
|
|
13
|
+
case 'agent.tool':
|
|
14
|
+
return `agent.tool(${event.toolName}, ${event.durationMs}ms)`;
|
|
15
|
+
case 'agent.llm':
|
|
16
|
+
return `agent.llm(${event.model}, in=${event.inputTokens}, out=${event.outputTokens})`;
|
|
17
|
+
case 'command':
|
|
18
|
+
return `command(${event.attributes['command.name']}, ${event.attributes['command.duration_ms']}ms, success=${event.attributes['command.success']})`;
|
|
19
|
+
case 'crash':
|
|
20
|
+
return `crash(${event.attributes['crash.error_type']}: ${event.attributes['crash.error_message']})`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
3
23
|
/**
|
|
4
|
-
* HTTP client that queues telemetry events and flushes them to the
|
|
24
|
+
* HTTP client that queues telemetry events and flushes them to the API.
|
|
5
25
|
* Failures are silent—telemetry should never crash the wizard.
|
|
6
26
|
*/
|
|
7
27
|
export class TelemetryClient {
|
|
8
28
|
events = [];
|
|
29
|
+
flushInFlight = null;
|
|
9
30
|
accessToken = null;
|
|
31
|
+
claimToken = null;
|
|
32
|
+
clientId = null;
|
|
33
|
+
apiKey = null;
|
|
10
34
|
gatewayUrl = null;
|
|
11
35
|
setGatewayUrl(url) {
|
|
12
36
|
this.gatewayUrl = url;
|
|
@@ -14,48 +38,134 @@ export class TelemetryClient {
|
|
|
14
38
|
setAccessToken(token) {
|
|
15
39
|
this.accessToken = token;
|
|
16
40
|
}
|
|
41
|
+
setApiKeyAuth(apiKey) {
|
|
42
|
+
this.apiKey = apiKey;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Set claim-token auth for unclaimed environments.
|
|
46
|
+
* The API accepts either a JWT (Bearer), claim token
|
|
47
|
+
* (x-workos-claim-token + x-workos-client-id), or API key
|
|
48
|
+
* (x-workos-api-key).
|
|
49
|
+
*/
|
|
50
|
+
setClaimTokenAuth(clientId, claimToken) {
|
|
51
|
+
this.clientId = clientId;
|
|
52
|
+
this.claimToken = claimToken;
|
|
53
|
+
}
|
|
17
54
|
queueEvent(event) {
|
|
18
55
|
this.events.push(event);
|
|
19
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Queue multiple pre-formed events (used by store-forward recovery).
|
|
59
|
+
*/
|
|
60
|
+
queueEvents(events) {
|
|
61
|
+
this.events.push(...events);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Flush queued events. Returns true if events were sent or intentionally
|
|
65
|
+
* dropped (4xx), false if they should be retried (5xx/network error).
|
|
66
|
+
* Uses splice to only remove the events that were in the snapshot,
|
|
67
|
+
* protecting any events queued concurrently during the fetch.
|
|
68
|
+
*/
|
|
20
69
|
async flush() {
|
|
70
|
+
// Coalesce overlapping flushes: a second caller during an in-flight flush
|
|
71
|
+
// would otherwise snapshot and POST the same events again (duplicate send),
|
|
72
|
+
// and its splice() could drop events queued after the first flush started.
|
|
73
|
+
if (this.flushInFlight)
|
|
74
|
+
return this.flushInFlight;
|
|
75
|
+
this.flushInFlight = this.flushInternal();
|
|
76
|
+
try {
|
|
77
|
+
return await this.flushInFlight;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
this.flushInFlight = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async flushInternal() {
|
|
21
84
|
if (this.events.length === 0)
|
|
22
|
-
return;
|
|
85
|
+
return true;
|
|
23
86
|
if (!this.gatewayUrl) {
|
|
24
|
-
debug('[Telemetry] No
|
|
25
|
-
return;
|
|
87
|
+
debug('[Telemetry] No telemetry URL configured, skipping flush');
|
|
88
|
+
return false;
|
|
26
89
|
}
|
|
27
|
-
const
|
|
28
|
-
this.events
|
|
90
|
+
const count = this.events.length;
|
|
91
|
+
const payload = { events: this.events.slice(0, count) };
|
|
29
92
|
const headers = {
|
|
30
93
|
'Content-Type': 'application/json',
|
|
31
94
|
};
|
|
32
|
-
// Read fresh credentials to handle token refresh mid-session
|
|
95
|
+
// Read fresh credentials to handle token refresh mid-session. Skip an
|
|
96
|
+
// expired stored token — sending a dead Bearer 401s and the event is
|
|
97
|
+
// dropped, so fall through to claim-token / api-key auth instead.
|
|
33
98
|
const freshCreds = getCredentials();
|
|
34
|
-
const token = freshCreds?.accessToken
|
|
99
|
+
const token = freshCreds?.accessToken
|
|
100
|
+
? isTokenExpired(freshCreds)
|
|
101
|
+
? null
|
|
102
|
+
: freshCreds.accessToken
|
|
103
|
+
: this.accessToken;
|
|
35
104
|
if (token) {
|
|
36
105
|
headers['Authorization'] = `Bearer ${token}`;
|
|
37
106
|
}
|
|
107
|
+
else if (this.claimToken && this.clientId) {
|
|
108
|
+
// Unclaimed environment auth path — guard accepts this instead of JWT
|
|
109
|
+
headers['x-workos-claim-token'] = this.claimToken;
|
|
110
|
+
headers['x-workos-client-id'] = this.clientId;
|
|
111
|
+
}
|
|
112
|
+
else if (this.apiKey) {
|
|
113
|
+
headers['x-workos-api-key'] = this.apiKey;
|
|
114
|
+
}
|
|
38
115
|
const controller = new AbortController();
|
|
39
116
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
40
117
|
try {
|
|
41
|
-
|
|
118
|
+
if (isDebugEnabled()) {
|
|
119
|
+
const eventSummary = payload.events.map(summarizeEvent).join('\n ');
|
|
120
|
+
debug(`[Telemetry] Sending ${payload.events.length} events to ${this.gatewayUrl}/telemetry:\n ${eventSummary}`);
|
|
121
|
+
}
|
|
42
122
|
const response = await fetch(`${this.gatewayUrl}/telemetry`, {
|
|
43
123
|
method: 'POST',
|
|
44
124
|
headers,
|
|
45
125
|
body: JSON.stringify(payload),
|
|
46
126
|
signal: controller.signal,
|
|
47
127
|
});
|
|
48
|
-
if (
|
|
128
|
+
if (response.ok) {
|
|
129
|
+
this.events.splice(0, count);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
49
133
|
debug(`[Telemetry] Failed to send: ${response.status}`);
|
|
134
|
+
// Drop on 4xx (permanent failures like 401/403 won't succeed on retry).
|
|
135
|
+
// Retain on 5xx (transient server errors) for store-forward.
|
|
136
|
+
if (response.status >= 400 && response.status < 500) {
|
|
137
|
+
this.events.splice(0, count);
|
|
138
|
+
return true; // intentionally dropped
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
50
141
|
}
|
|
51
142
|
}
|
|
52
143
|
catch (error) {
|
|
53
144
|
debug(`[Telemetry] Error sending events: ${error}`);
|
|
145
|
+
// Events remain in queue for store-forward to persist
|
|
146
|
+
return false;
|
|
54
147
|
}
|
|
55
148
|
finally {
|
|
56
149
|
clearTimeout(timeout);
|
|
57
150
|
}
|
|
58
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Synchronously write pending events to a file.
|
|
154
|
+
* Used as last resort in process.on('exit') handler.
|
|
155
|
+
*/
|
|
156
|
+
persistToFile(filePath) {
|
|
157
|
+
if (this.events.length === 0)
|
|
158
|
+
return;
|
|
159
|
+
try {
|
|
160
|
+
// Restrictive modes — the payload carries device/user identifiers.
|
|
161
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
162
|
+
writeFileSync(filePath, JSON.stringify(this.events), { encoding: 'utf-8', mode: 0o600 });
|
|
163
|
+
this.events = [];
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Silent failure — telemetry must never block exit
|
|
167
|
+
}
|
|
168
|
+
}
|
|
59
169
|
}
|
|
60
170
|
export const telemetryClient = new TelemetryClient();
|
|
61
171
|
//# sourceMappingURL=telemetry-client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry-client.js","sourceRoot":"","sources":["../../src/utils/telemetry-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD;;;GAGG;AACH,MAAM,OAAO,eAAe;IAClB,MAAM,GAAqB,EAAE,CAAC;IAC9B,WAAW,GAAkB,IAAI,CAAC;IAClC,UAAU,GAAkB,IAAI,CAAC;IAEzC,aAAa,CAAC,GAAW;QACvB,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;IACxB,CAAC;IAED,cAAc,CAAC,KAAa;QAC1B,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;IAC3B,CAAC;IAED,UAAU,CAAC,KAAqB;QAC9B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACrC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,KAAK,CAAC,uDAAuD,CAAC,CAAC;YAC/D,OAAO;QACT,CAAC;QAED,MAAM,OAAO,GAAqB,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/D,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QAEjB,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;SACnC,CAAC;QACF,6DAA6D;QAC7D,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,UAAU,EAAE,WAAW,IAAI,IAAI,CAAC,WAAW,CAAC;QAC1D,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,KAAK,EAAE,CAAC;QAC/C,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;QAE3D,IAAI,CAAC;YACH,KAAK,CAAC,uBAAuB,OAAO,CAAC,MAAM,CAAC,MAAM,cAAc,IAAI,CAAC,UAAU,YAAY,CAAC,CAAC;YAE7F,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,UAAU,YAAY,EAAE;gBAC3D,MAAM,EAAE,MAAM;gBACd,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;gBAC7B,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,KAAK,CAAC,+BAA+B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,KAAK,CAAC,qCAAqC,KAAK,EAAE,CAAC,CAAC;QACtD,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;CACF;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC","sourcesContent":["import { debug } from './debug.js';\nimport type { TelemetryEvent, TelemetryRequest } from './telemetry-types.js';\nimport { getCredentials } from '../lib/credentials.js';\n\n/**\n * HTTP client that queues telemetry events and flushes them to the gateway.\n * Failures are silent—telemetry should never crash the wizard.\n */\nexport class TelemetryClient {\n private events: TelemetryEvent[] = [];\n private accessToken: string | null = null;\n private gatewayUrl: string | null = null;\n\n setGatewayUrl(url: string) {\n this.gatewayUrl = url;\n }\n\n setAccessToken(token: string) {\n this.accessToken = token;\n }\n\n queueEvent(event: TelemetryEvent) {\n this.events.push(event);\n }\n\n async flush(): Promise<void> {\n if (this.events.length === 0) return;\n if (!this.gatewayUrl) {\n debug('[Telemetry] No gateway URL configured, skipping flush');\n return;\n }\n\n const payload: TelemetryRequest = { events: [...this.events] };\n this.events = [];\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n // Read fresh credentials to handle token refresh mid-session\n const freshCreds = getCredentials();\n const token = freshCreds?.accessToken ?? this.accessToken;\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 3000);\n\n try {\n debug(`[Telemetry] Sending ${payload.events.length} events to ${this.gatewayUrl}/telemetry`);\n\n const response = await fetch(`${this.gatewayUrl}/telemetry`, {\n method: 'POST',\n headers,\n body: JSON.stringify(payload),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n debug(`[Telemetry] Failed to send: ${response.status}`);\n }\n } catch (error) {\n debug(`[Telemetry] Error sending events: ${error}`);\n } finally {\n clearTimeout(timeout);\n }\n }\n}\n\nexport const telemetryClient = new TelemetryClient();\n"]}
|
|
1
|
+
{"version":3,"file":"telemetry-client.js","sourceRoot":"","sources":["../../src/utils/telemetry-client.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport { debug, isDebugEnabled } from './debug.js';\nimport type { TelemetryEvent, TelemetryRequest } from './telemetry-types.js';\nimport { getCredentials, isTokenExpired } from '../lib/credentials.js';\n\nfunction summarizeEvent(event: TelemetryEvent): string {\n switch (event.type) {\n case 'session.start':\n return `session.start(mode=${event.attributes['installer.mode']}, os=${event.attributes['env.os']})`;\n case 'session.end':\n return `session.end(outcome=${event.attributes['installer.outcome']}, duration=${event.attributes['installer.duration_ms']}ms)`;\n case 'step':\n return `step(${event.name}, ${event.durationMs}ms, success=${event.success})`;\n case 'agent.tool':\n return `agent.tool(${event.toolName}, ${event.durationMs}ms)`;\n case 'agent.llm':\n return `agent.llm(${event.model}, in=${event.inputTokens}, out=${event.outputTokens})`;\n case 'command':\n return `command(${event.attributes['command.name']}, ${event.attributes['command.duration_ms']}ms, success=${event.attributes['command.success']})`;\n case 'crash':\n return `crash(${event.attributes['crash.error_type']}: ${event.attributes['crash.error_message']})`;\n }\n}\n\n/**\n * HTTP client that queues telemetry events and flushes them to the API.\n * Failures are silent—telemetry should never crash the wizard.\n */\nexport class TelemetryClient {\n private events: TelemetryEvent[] = [];\n private flushInFlight: Promise<boolean> | null = null;\n private accessToken: string | null = null;\n private claimToken: string | null = null;\n private clientId: string | null = null;\n private apiKey: string | null = null;\n private gatewayUrl: string | null = null;\n\n setGatewayUrl(url: string) {\n this.gatewayUrl = url;\n }\n\n setAccessToken(token: string) {\n this.accessToken = token;\n }\n\n setApiKeyAuth(apiKey: string) {\n this.apiKey = apiKey;\n }\n\n /**\n * Set claim-token auth for unclaimed environments.\n * The API accepts either a JWT (Bearer), claim token\n * (x-workos-claim-token + x-workos-client-id), or API key\n * (x-workos-api-key).\n */\n setClaimTokenAuth(clientId: string, claimToken: string) {\n this.clientId = clientId;\n this.claimToken = claimToken;\n }\n\n queueEvent(event: TelemetryEvent) {\n this.events.push(event);\n }\n\n /**\n * Queue multiple pre-formed events (used by store-forward recovery).\n */\n queueEvents(events: TelemetryEvent[]): void {\n this.events.push(...events);\n }\n\n /**\n * Flush queued events. Returns true if events were sent or intentionally\n * dropped (4xx), false if they should be retried (5xx/network error).\n * Uses splice to only remove the events that were in the snapshot,\n * protecting any events queued concurrently during the fetch.\n */\n async flush(): Promise<boolean> {\n // Coalesce overlapping flushes: a second caller during an in-flight flush\n // would otherwise snapshot and POST the same events again (duplicate send),\n // and its splice() could drop events queued after the first flush started.\n if (this.flushInFlight) return this.flushInFlight;\n this.flushInFlight = this.flushInternal();\n try {\n return await this.flushInFlight;\n } finally {\n this.flushInFlight = null;\n }\n }\n\n private async flushInternal(): Promise<boolean> {\n if (this.events.length === 0) return true;\n if (!this.gatewayUrl) {\n debug('[Telemetry] No telemetry URL configured, skipping flush');\n return false;\n }\n\n const count = this.events.length;\n const payload: TelemetryRequest = { events: this.events.slice(0, count) };\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n // Read fresh credentials to handle token refresh mid-session. Skip an\n // expired stored token — sending a dead Bearer 401s and the event is\n // dropped, so fall through to claim-token / api-key auth instead.\n const freshCreds = getCredentials();\n const token = freshCreds?.accessToken\n ? isTokenExpired(freshCreds)\n ? null\n : freshCreds.accessToken\n : this.accessToken;\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n } else if (this.claimToken && this.clientId) {\n // Unclaimed environment auth path — guard accepts this instead of JWT\n headers['x-workos-claim-token'] = this.claimToken;\n headers['x-workos-client-id'] = this.clientId;\n } else if (this.apiKey) {\n headers['x-workos-api-key'] = this.apiKey;\n }\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 3000);\n\n try {\n if (isDebugEnabled()) {\n const eventSummary = payload.events.map(summarizeEvent).join('\\n ');\n debug(\n `[Telemetry] Sending ${payload.events.length} events to ${this.gatewayUrl}/telemetry:\\n ${eventSummary}`,\n );\n }\n\n const response = await fetch(`${this.gatewayUrl}/telemetry`, {\n method: 'POST',\n headers,\n body: JSON.stringify(payload),\n signal: controller.signal,\n });\n\n if (response.ok) {\n this.events.splice(0, count);\n return true;\n } else {\n debug(`[Telemetry] Failed to send: ${response.status}`);\n // Drop on 4xx (permanent failures like 401/403 won't succeed on retry).\n // Retain on 5xx (transient server errors) for store-forward.\n if (response.status >= 400 && response.status < 500) {\n this.events.splice(0, count);\n return true; // intentionally dropped\n }\n return false;\n }\n } catch (error) {\n debug(`[Telemetry] Error sending events: ${error}`);\n // Events remain in queue for store-forward to persist\n return false;\n } finally {\n clearTimeout(timeout);\n }\n }\n\n /**\n * Synchronously write pending events to a file.\n * Used as last resort in process.on('exit') handler.\n */\n persistToFile(filePath: string): void {\n if (this.events.length === 0) return;\n try {\n // Restrictive modes — the payload carries device/user identifiers.\n mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });\n writeFileSync(filePath, JSON.stringify(this.events), { encoding: 'utf-8', mode: 0o600 });\n this.events = [];\n } catch {\n // Silent failure — telemetry must never block exit\n }\n }\n}\n\nexport const telemetryClient = new TelemetryClient();\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register a sync exit handler that persists unsent events to disk.
|
|
3
|
+
* Called once at startup. Uses PID in filename to prevent concurrent
|
|
4
|
+
* CLI invocations from colliding.
|
|
5
|
+
*/
|
|
6
|
+
export declare function installStoreForward(): void;
|
|
7
|
+
/**
|
|
8
|
+
* On startup, check for ANY pending files from previous invocations
|
|
9
|
+
* (could be from different PIDs) and send them. Non-blocking, fire-and-forget.
|
|
10
|
+
*/
|
|
11
|
+
export declare function recoverPendingEvents(): Promise<void>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync, unlinkSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { telemetryClient } from './telemetry-client.js';
|
|
5
|
+
import { debug } from './debug.js';
|
|
6
|
+
const PENDING_DIR = join(tmpdir(), 'workos-cli-telemetry');
|
|
7
|
+
const PENDING_FILE = join(PENDING_DIR, `pending-${process.pid}.json`);
|
|
8
|
+
const MAX_PENDING_FILES = 100;
|
|
9
|
+
const MAX_PENDING_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
10
|
+
function safeUnlink(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
unlinkSync(filePath);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
/* ignore */
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Register a sync exit handler that persists unsent events to disk.
|
|
20
|
+
* Called once at startup. Uses PID in filename to prevent concurrent
|
|
21
|
+
* CLI invocations from colliding.
|
|
22
|
+
*/
|
|
23
|
+
export function installStoreForward() {
|
|
24
|
+
process.on('exit', () => {
|
|
25
|
+
telemetryClient.persistToFile(PENDING_FILE);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* On startup, check for ANY pending files from previous invocations
|
|
30
|
+
* (could be from different PIDs) and send them. Non-blocking, fire-and-forget.
|
|
31
|
+
*/
|
|
32
|
+
export async function recoverPendingEvents() {
|
|
33
|
+
try {
|
|
34
|
+
if (!existsSync(PENDING_DIR))
|
|
35
|
+
return;
|
|
36
|
+
const files = readdirSync(PENDING_DIR).filter((f) => f.startsWith('pending-') && f.endsWith('.json'));
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
const pendingFiles = [];
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
const filePath = join(PENDING_DIR, file);
|
|
41
|
+
try {
|
|
42
|
+
const { mtimeMs } = statSync(filePath);
|
|
43
|
+
if (now - mtimeMs > MAX_PENDING_AGE_MS) {
|
|
44
|
+
debug(`[Telemetry] Dropping stale pending file: ${file}`);
|
|
45
|
+
safeUnlink(filePath);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
pendingFiles.push({ file, filePath, mtimeMs });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
debug(`[Telemetry] Dropping unreadable pending file: ${file}`);
|
|
53
|
+
safeUnlink(filePath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
pendingFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
57
|
+
const filesToRecover = pendingFiles.slice(0, MAX_PENDING_FILES);
|
|
58
|
+
for (const dropped of pendingFiles.slice(MAX_PENDING_FILES)) {
|
|
59
|
+
debug(`[Telemetry] Dropping excess pending file: ${dropped.file}`);
|
|
60
|
+
safeUnlink(dropped.filePath);
|
|
61
|
+
}
|
|
62
|
+
const recoveredFiles = [];
|
|
63
|
+
for (const { filePath } of filesToRecover) {
|
|
64
|
+
try {
|
|
65
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
66
|
+
const events = JSON.parse(raw);
|
|
67
|
+
if (Array.isArray(events) && events.length > 0) {
|
|
68
|
+
telemetryClient.queueEvents(events);
|
|
69
|
+
recoveredFiles.push(filePath);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Empty file — delete immediately
|
|
73
|
+
safeUnlink(filePath);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Corrupted file — delete and move on
|
|
78
|
+
safeUnlink(filePath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Delete source files — events are now in memory regardless of flush outcome.
|
|
82
|
+
// If flush succeeds: events sent, done.
|
|
83
|
+
// If flush fails: events stay in memory, exit handler re-persists to new PID file.
|
|
84
|
+
for (const filePath of recoveredFiles) {
|
|
85
|
+
safeUnlink(filePath);
|
|
86
|
+
}
|
|
87
|
+
// Flush all recovered events in one batch
|
|
88
|
+
await telemetryClient.flush();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
debug('[Telemetry] Store-forward recovery failed silently');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=telemetry-store-forward.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-store-forward.js","sourceRoot":"","sources":["../../src/utils/telemetry-store-forward.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync, readdirSync, statSync, unlinkSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { telemetryClient } from './telemetry-client.js';\nimport { debug } from './debug.js';\n\nconst PENDING_DIR = join(tmpdir(), 'workos-cli-telemetry');\nconst PENDING_FILE = join(PENDING_DIR, `pending-${process.pid}.json`);\nconst MAX_PENDING_FILES = 100;\nconst MAX_PENDING_AGE_MS = 7 * 24 * 60 * 60 * 1000;\n\nfunction safeUnlink(filePath: string): void {\n try {\n unlinkSync(filePath);\n } catch {\n /* ignore */\n }\n}\n\n/**\n * Register a sync exit handler that persists unsent events to disk.\n * Called once at startup. Uses PID in filename to prevent concurrent\n * CLI invocations from colliding.\n */\nexport function installStoreForward(): void {\n process.on('exit', () => {\n telemetryClient.persistToFile(PENDING_FILE);\n });\n}\n\n/**\n * On startup, check for ANY pending files from previous invocations\n * (could be from different PIDs) and send them. Non-blocking, fire-and-forget.\n */\nexport async function recoverPendingEvents(): Promise<void> {\n try {\n if (!existsSync(PENDING_DIR)) return;\n const files = readdirSync(PENDING_DIR).filter((f) => f.startsWith('pending-') && f.endsWith('.json'));\n const now = Date.now();\n const pendingFiles: Array<{ file: string; filePath: string; mtimeMs: number }> = [];\n\n for (const file of files) {\n const filePath = join(PENDING_DIR, file);\n try {\n const { mtimeMs } = statSync(filePath);\n if (now - mtimeMs > MAX_PENDING_AGE_MS) {\n debug(`[Telemetry] Dropping stale pending file: ${file}`);\n safeUnlink(filePath);\n } else {\n pendingFiles.push({ file, filePath, mtimeMs });\n }\n } catch {\n debug(`[Telemetry] Dropping unreadable pending file: ${file}`);\n safeUnlink(filePath);\n }\n }\n\n pendingFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);\n const filesToRecover = pendingFiles.slice(0, MAX_PENDING_FILES);\n for (const dropped of pendingFiles.slice(MAX_PENDING_FILES)) {\n debug(`[Telemetry] Dropping excess pending file: ${dropped.file}`);\n safeUnlink(dropped.filePath);\n }\n\n const recoveredFiles: string[] = [];\n for (const { filePath } of filesToRecover) {\n try {\n const raw = readFileSync(filePath, 'utf-8');\n const events = JSON.parse(raw);\n if (Array.isArray(events) && events.length > 0) {\n telemetryClient.queueEvents(events);\n recoveredFiles.push(filePath);\n } else {\n // Empty file — delete immediately\n safeUnlink(filePath);\n }\n } catch {\n // Corrupted file — delete and move on\n safeUnlink(filePath);\n }\n }\n\n // Delete source files — events are now in memory regardless of flush outcome.\n // If flush succeeds: events sent, done.\n // If flush fails: events stay in memory, exit handler re-persists to new PID file.\n for (const filePath of recoveredFiles) {\n safeUnlink(filePath);\n }\n\n // Flush all recovered events in one batch\n await telemetryClient.flush();\n } catch {\n debug('[Telemetry] Store-forward recovery failed silently');\n }\n}\n"]}
|
|
@@ -2,31 +2,49 @@
|
|
|
2
2
|
* Telemetry event types for installer → gateway communication.
|
|
3
3
|
* The gateway converts these to OTel format.
|
|
4
4
|
*/
|
|
5
|
-
export interface
|
|
6
|
-
type: 'session.start' | 'session.end' | 'step' | 'agent.tool' | 'agent.llm';
|
|
5
|
+
export interface BaseTelemetryEvent {
|
|
6
|
+
type: 'session.start' | 'session.end' | 'step' | 'agent.tool' | 'agent.llm' | 'command' | 'crash';
|
|
7
7
|
sessionId: string;
|
|
8
8
|
timestamp: string;
|
|
9
|
-
attributes?: Record<string, string | number | boolean>;
|
|
10
9
|
}
|
|
11
|
-
export
|
|
10
|
+
export type AuthMode = 'jwt' | 'claim_token' | 'api_key' | 'none';
|
|
11
|
+
/**
|
|
12
|
+
* Structured outcome dimension for command events. Supersedes the boolean
|
|
13
|
+
* `command.success` as the primary categorization (`command.success` remains
|
|
14
|
+
* for backward-compat). Populated by `analytics.emitCommandEvent()` from the
|
|
15
|
+
* top-level command lifecycle.
|
|
16
|
+
*/
|
|
17
|
+
export type TerminationReason = 'success' | 'cancelled' | 'auth_required' | 'validation_error' | 'api_error' | 'crash';
|
|
18
|
+
export interface EnvFingerprint {
|
|
19
|
+
'device.id': string;
|
|
20
|
+
'auth.mode': AuthMode;
|
|
21
|
+
'env.os': string;
|
|
22
|
+
'env.os_version': string;
|
|
23
|
+
'env.node_version': string;
|
|
24
|
+
'env.shell': string;
|
|
25
|
+
'env.ci': boolean;
|
|
26
|
+
'env.ci_provider'?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SessionStartEvent extends BaseTelemetryEvent {
|
|
12
29
|
type: 'session.start';
|
|
13
30
|
attributes: {
|
|
14
31
|
'installer.version': string;
|
|
15
32
|
'installer.mode': 'cli' | 'tui' | 'headless';
|
|
16
33
|
'workos.user_id'?: string;
|
|
17
34
|
'workos.org_id'?: string;
|
|
18
|
-
};
|
|
35
|
+
} & EnvFingerprint;
|
|
19
36
|
}
|
|
20
|
-
export interface SessionEndEvent extends
|
|
37
|
+
export interface SessionEndEvent extends BaseTelemetryEvent {
|
|
21
38
|
type: 'session.end';
|
|
22
39
|
attributes: {
|
|
23
40
|
'installer.outcome': 'success' | 'error' | 'cancelled';
|
|
24
41
|
'installer.duration_ms': number;
|
|
25
42
|
} & Record<string, string | number | boolean>;
|
|
26
43
|
}
|
|
27
|
-
export interface StepEvent extends
|
|
44
|
+
export interface StepEvent extends BaseTelemetryEvent {
|
|
28
45
|
type: 'step';
|
|
29
46
|
name: string;
|
|
47
|
+
startTimestamp: string;
|
|
30
48
|
durationMs: number;
|
|
31
49
|
success: boolean;
|
|
32
50
|
error?: {
|
|
@@ -34,18 +52,49 @@ export interface StepEvent extends TelemetryEvent {
|
|
|
34
52
|
message: string;
|
|
35
53
|
};
|
|
36
54
|
}
|
|
37
|
-
export interface AgentToolEvent extends
|
|
55
|
+
export interface AgentToolEvent extends BaseTelemetryEvent {
|
|
38
56
|
type: 'agent.tool';
|
|
39
57
|
toolName: string;
|
|
58
|
+
startTimestamp: string;
|
|
40
59
|
durationMs: number;
|
|
41
60
|
success: boolean;
|
|
42
61
|
}
|
|
43
|
-
export interface AgentLLMEvent extends
|
|
62
|
+
export interface AgentLLMEvent extends BaseTelemetryEvent {
|
|
44
63
|
type: 'agent.llm';
|
|
45
64
|
model: string;
|
|
46
65
|
inputTokens: number;
|
|
47
66
|
outputTokens: number;
|
|
48
67
|
}
|
|
68
|
+
export interface CommandEvent extends BaseTelemetryEvent {
|
|
69
|
+
type: 'command';
|
|
70
|
+
attributes: {
|
|
71
|
+
'command.name': string;
|
|
72
|
+
'command.duration_ms': number;
|
|
73
|
+
'command.success': boolean;
|
|
74
|
+
'command.error_type'?: string;
|
|
75
|
+
'command.error_message'?: string;
|
|
76
|
+
'command.flags'?: string;
|
|
77
|
+
'termination.reason'?: TerminationReason;
|
|
78
|
+
'error.code'?: string;
|
|
79
|
+
'api.status'?: number;
|
|
80
|
+
'api.code'?: string;
|
|
81
|
+
'api.resource'?: string;
|
|
82
|
+
'cli.version': string;
|
|
83
|
+
'workos.user_id'?: string;
|
|
84
|
+
} & EnvFingerprint;
|
|
85
|
+
}
|
|
86
|
+
export interface CrashEvent extends BaseTelemetryEvent {
|
|
87
|
+
type: 'crash';
|
|
88
|
+
attributes: {
|
|
89
|
+
'crash.error_type': string;
|
|
90
|
+
'crash.error_message': string;
|
|
91
|
+
'crash.stack': string;
|
|
92
|
+
'crash.command'?: string;
|
|
93
|
+
'cli.version': string;
|
|
94
|
+
'workos.user_id'?: string;
|
|
95
|
+
} & EnvFingerprint;
|
|
96
|
+
}
|
|
49
97
|
export interface TelemetryRequest {
|
|
50
98
|
events: TelemetryEvent[];
|
|
51
99
|
}
|
|
100
|
+
export type TelemetryEvent = SessionStartEvent | SessionEndEvent | StepEvent | AgentToolEvent | AgentLLMEvent | CommandEvent | CrashEvent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry-types.js","sourceRoot":"","sources":["../../src/utils/telemetry-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * Telemetry event types for installer → gateway communication.\n * The gateway converts these to OTel format.\n */\n\nexport interface
|
|
1
|
+
{"version":3,"file":"telemetry-types.js","sourceRoot":"","sources":["../../src/utils/telemetry-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * Telemetry event types for installer → gateway communication.\n * The gateway converts these to OTel format.\n */\n\nexport interface BaseTelemetryEvent {\n type: 'session.start' | 'session.end' | 'step' | 'agent.tool' | 'agent.llm' | 'command' | 'crash';\n sessionId: string;\n timestamp: string;\n}\n\nexport type AuthMode = 'jwt' | 'claim_token' | 'api_key' | 'none';\n\n/**\n * Structured outcome dimension for command events. Supersedes the boolean\n * `command.success` as the primary categorization (`command.success` remains\n * for backward-compat). Populated by `analytics.emitCommandEvent()` from the\n * top-level command lifecycle.\n */\nexport type TerminationReason = 'success' | 'cancelled' | 'auth_required' | 'validation_error' | 'api_error' | 'crash';\n\nexport interface EnvFingerprint {\n 'device.id': string;\n 'auth.mode': AuthMode;\n 'env.os': string;\n 'env.os_version': string;\n 'env.node_version': string;\n 'env.shell': string;\n 'env.ci': boolean;\n 'env.ci_provider'?: string;\n}\n\nexport interface SessionStartEvent extends BaseTelemetryEvent {\n type: 'session.start';\n attributes: {\n 'installer.version': string;\n 'installer.mode': 'cli' | 'tui' | 'headless';\n 'workos.user_id'?: string;\n 'workos.org_id'?: string;\n } & EnvFingerprint;\n}\n\nexport interface SessionEndEvent extends BaseTelemetryEvent {\n type: 'session.end';\n attributes: {\n 'installer.outcome': 'success' | 'error' | 'cancelled';\n 'installer.duration_ms': number;\n } & Record<string, string | number | boolean>;\n}\n\nexport interface StepEvent extends BaseTelemetryEvent {\n type: 'step';\n name: string;\n startTimestamp: string;\n durationMs: number;\n success: boolean;\n error?: {\n type: string;\n message: string;\n };\n}\n\nexport interface AgentToolEvent extends BaseTelemetryEvent {\n type: 'agent.tool';\n toolName: string;\n startTimestamp: string;\n durationMs: number;\n success: boolean;\n}\n\nexport interface AgentLLMEvent extends BaseTelemetryEvent {\n type: 'agent.llm';\n model: string;\n inputTokens: number;\n outputTokens: number;\n}\n\nexport interface CommandEvent extends BaseTelemetryEvent {\n type: 'command';\n attributes: {\n 'command.name': string;\n 'command.duration_ms': number;\n 'command.success': boolean;\n 'command.error_type'?: string;\n 'command.error_message'?: string;\n 'command.flags'?: string;\n 'termination.reason'?: TerminationReason;\n 'error.code'?: string;\n 'api.status'?: number;\n 'api.code'?: string;\n 'api.resource'?: string;\n 'cli.version': string;\n 'workos.user_id'?: string;\n } & EnvFingerprint;\n}\n\nexport interface CrashEvent extends BaseTelemetryEvent {\n type: 'crash';\n attributes: {\n 'crash.error_type': string;\n 'crash.error_message': string;\n 'crash.stack': string;\n 'crash.command'?: string;\n 'cli.version': string;\n 'workos.user_id'?: string;\n } & EnvFingerprint;\n}\n\nexport interface TelemetryRequest {\n events: TelemetryEvent[];\n}\n\nexport type TelemetryEvent =\n | SessionStartEvent\n | SessionEndEvent\n | StepEvent\n | AgentToolEvent\n | AgentLLMEvent\n | CommandEvent\n | CrashEvent;\n"]}
|