feat(analytics): attribute requests by client surface and add CLI usa… · simstudioai/sim@3dafafd · GitHub
Skip to content

Commit 3dafafd

Browse files
authored
feat(analytics): attribute requests by client surface and add CLI usage telemetry (#7763)
* feat(analytics): attribute requests by client surface and add CLI usage telemetry * fix(analytics): tolerate get-only header readers, gate CLI notice on stderr, re-read telemetry state before send
1 parent b4f214c commit 3dafafd

95 files changed

Lines changed: 3000 additions & 236 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/publish-sim-cli.yml‎

Lines changed: 5 additions & 0 deletions
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
// client-info pulls in @/main/navigation, which imports electron.
4+
vi.mock('electron', () => import('@/test/electron-mock'))
5+
6+
import { attachClientInfo, desktopClientInfo } from '@/main/client-info'
7+
8+
type BeforeSendHeadersHandler = (
9+
details: { url: string; requestHeaders: Record<string, string> },
10+
callback: (response: { requestHeaders?: Record<string, string> }) => void
11+
) => void
12+
13+
function fakeSession() {
14+
let handler: BeforeSendHeadersHandler | undefined
15+
const ses = {
16+
webRequest: {
17+
onBeforeSendHeaders: vi.fn((h: BeforeSendHeadersHandler) => {
18+
handler = h
19+
}),
20+
},
21+
}
22+
return { ses, run: () => handler }
23+
}
24+
25+
const APP_ORIGIN = 'https://sim.ai'
26+
const CLIENT_INFO = desktopClientInfo()
27+
28+
describe('desktopClientInfo', () => {
29+
it('names the shell, its runtime, and the platform', () => {
30+
expect(desktopClientInfo()).toMatch(
31+
new RegExp(
32+
`^desktop/1\\.0\\.0(; electron/[^;]+)?; os/${process.platform}; arch/${process.arch}$`
33+
)
34+
)
35+
})
36+
})
37+
38+
describe('attachClientInfo', () => {
39+
let session: ReturnType<typeof fakeSession>
40+
41+
beforeEach(() => {
42+
session = fakeSession()
43+
attachClientInfo(
44+
session.ses as unknown as Parameters<typeof attachClientInfo>[0],
45+
() => APP_ORIGIN
46+
)
47+
})
48+
49+
it('stamps the shell identity on an app-origin request', () => {
50+
const cb = vi.fn()
51+
session.run()?.(
52+
{ url: `${APP_ORIGIN}/api/workflows`, requestHeaders: { Accept: 'application/json' } },
53+
cb
54+
)
55+
expect(cb).toHaveBeenCalledWith({
56+
requestHeaders: { Accept: 'application/json', 'x-sim-client-info': CLIENT_INFO },
57+
})
58+
})
59+
60+
it('overwrites the web value the page sent, whatever its casing', () => {
61+
const cb = vi.fn()
62+
session.run()?.(
63+
{ url: `${APP_ORIGIN}/api/workflows`, requestHeaders: { 'X-Sim-Client-Info': 'web' } },
64+
cb
65+
)
66+
expect(cb).toHaveBeenCalledWith({
67+
requestHeaders: { 'x-sim-client-info': CLIENT_INFO },
68+
})
69+
})
70+
71+
it('leaves requests to other origins untouched', () => {
72+
const cb = vi.fn()
73+
session.run()?.(
74+
{ url: 'https://accounts.google.com/o/oauth2', requestHeaders: { Accept: '*/*' } },
75+
cb
76+
)
77+
expect(cb).toHaveBeenCalledWith({})
78+
})
79+
})
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { CLIENT_INFO_HEADER, formatClientInfo } from '@sim/utils/client-info'
2+
import type { Session } from 'electron'
3+
import { app } from 'electron'
4+
import { isAppOrigin } from '@/main/navigation'
5+
6+
/**
7+
* The `X-Sim-Client-Info` value naming this shell: its version, the Electron
8+
* it runs on, and the platform. Computed once per process — none of it changes
9+
* while the app is running.
10+
*/
11+
export function desktopClientInfo(): string {
12+
const electron = process.versions.electron
13+
return formatClientInfo({
14+
surface: 'desktop',
15+
version: app.getVersion(),
16+
...(electron ? { runtime: { name: 'electron', version: electron } } : {}),
17+
os: process.platform,
18+
arch: process.arch,
19+
})
20+
}
21+
22+
/**
23+
* Stamps the shell's identity on every request to the app origin.
24+
*
25+
* The page derives the same value from the preload bridge, so this is the
26+
* backstop for what the page never issues itself — raw `fetch` exceptions,
27+
* service-worker traffic, sub-resources — and for a bundle older than the
28+
* bridge field. Only the network layer sees every request. The shell's value
29+
* overwrites whatever the page sent; the shell is authoritative about being
30+
* the shell. Requests to other origins are left untouched.
31+
*
32+
* This is the only `onBeforeSendHeaders` consumer — Electron allows a single
33+
* listener per session.
34+
*/
35+
export function attachClientInfo(ses: Session, appOrigin: () => string): void {
36+
const clientInfo = desktopClientInfo()
37+
ses.webRequest.onBeforeSendHeaders((details, callback) => {
38+
if (!isAppOrigin(details.url, appOrigin())) {
39+
callback({})
40+
return
41+
}
42+
const requestHeaders: Record<string, string> = {}
43+
for (const [name, value] of Object.entries(details.requestHeaders)) {
44+
if (name.toLowerCase() !== CLIENT_INFO_HEADER) requestHeaders[name] = value
45+
}
46+
requestHeaders[CLIENT_INFO_HEADER] = clientInfo
47+
callback({ requestHeaders })
48+
})
49+
}

‎apps/desktop/src/main/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
setBrowserAppearanceTheme as setAgentBrowserTheme,
3636
setPanelFocused as setBrowserAgentPanelFocused,
3737
} from '@/main/browser-agent/session'
38+
import { attachClientInfo } from '@/main/client-info'
3839
import {
3940
APP_NAME_FOR_CHANNEL,
4041
channelForOrigin,
@@ -265,6 +266,7 @@ function main(): void {
265266
setupPermissionHandlers(ses, appOrigin)
266267
attachLocalPageProtocol(ses)
267268
attachCspFallback(ses, appOrigin)
269+
attachClientInfo(ses, appOrigin)
268270
attachDownloadHandling(ses, events)
269271
attachTelemetryPolicy(ses, config.get('blockThirdPartyAnalytics') ?? true)
270272
ses.setSpellCheckerLanguages(['en-US'])

‎apps/docs/content/docs/cli/commands.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ These apply to every command, and may be written before or after it.
3131
| Group | Description |
3232
| --- | --- |
3333
| [`sim profiles`](/cli/profiles) | List profiles or add a workspace profile that shares a stored login |
34+
| [`sim telemetry`](/cli/telemetry) | Control anonymous usage reporting |
3435
| [`sim audit-logs`](/cli/audit-logs) | Manage audit logs |
3536
| [`sim billing`](/cli/billing) | Manage billing |
3637
| [`sim blocks`](/cli/blocks) | Manage blocks |

‎apps/docs/content/docs/cli/configuration.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ endpoint or stored login.
121121
| `SIM_TIMEOUT_SECONDS` | Per-request timeout; `0` waits indefinitely. Defaults to `3600`, above every timeout the server itself applies |
122122
| `SIM_DEBUG` | Trace each request's method, URL, status and duration to stderr |
123123
| `SIM_NO_UPDATE_CHECK` | Turn off update checks and notices |
124+
| `SIM_TELEMETRY_DISABLED` | Turn off anonymous usage reporting; see [usage data](/cli/usage-data) |
124125

125126
## Updates
126127

‎apps/docs/content/docs/cli/meta.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
"scripting",
1111
"workflow-sync",
1212
"troubleshooting",
13+
"usage-data",
1314
"---Commands---",
1415
"commands",
1516
"profiles",
17+
"telemetry",
1618
"audit-logs",
1719
"billing",
1820
"blocks",

‎apps/docs/content/docs/cli/reference.mdx‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,32 @@ sim profiles add <name> [options]
189189

190190
</CommandTable>
191191

192+
## sim telemetry
193+
194+
### sim telemetry status
195+
196+
Show whether usage reporting is on, and why not if it is off
197+
198+
```bash
199+
sim telemetry status
200+
```
201+
202+
### sim telemetry enable
203+
204+
Turn usage reporting on for this machine
205+
206+
```bash
207+
sim telemetry enable
208+
```
209+
210+
### sim telemetry disable
211+
212+
Turn usage reporting off for this machine
213+
214+
```bash
215+
sim telemetry disable
216+
```
217+
192218
## sim audit-logs
193219

194220
Also spelled `sim audit-log`.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
title: Telemetry
3+
description: Control anonymous usage reporting — every subcommand, argument, and flag
4+
---
5+
6+
import { CommandTable } from '@/components/ui/command-table'
7+
8+
Every command below also accepts the [global options](/cli/commands#global-options).
9+
10+
## Show whether usage reporting is on, and why not if it is off
11+
12+
```bash
13+
sim telemetry status
14+
```
15+
16+
## Turn usage reporting on for this machine
17+
18+
```bash
19+
sim telemetry enable
20+
```
21+
22+
## Turn usage reporting off for this machine
23+
24+
```bash
25+
sim telemetry disable
26+
```
Lines changed: 75 additions & 0 deletions

0 commit comments

Comments
 (0)