Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Usage data also goes to Scarf.** The same usage events GemDB already sends now go to
[Scarf](https://about.scarf.sh) as well, with fewer of VS Code's own properties. Turning
telemetry off in VS Code turns off both. [USAGE_DATA.md](USAGE_DATA.md) says what Scarf receives
and how to have your data deleted.
- **The new GemDB brand.** The Marketplace and Extensions view icon is GemDB's dark-mode favicon, the
gem on a blue gradient, and the activity bar shows the gem, recoloured to fit your theme with its
right face lighter as in the logo. The Marketplace page's banner is the brand's Core Purple.
Expand Down
39 changes: 28 additions & 11 deletions USAGE_DATA.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,21 @@ stores those three fields alongside each event. Your IP address itself is
do not use the location to identify anyone; it tells us which regions GemDB
is used in.

**Scarf.** The same events also go to [Scarf](https://about.scarf.sh), with
GemDB's own properties and five of VS Code's: `common.vscodemachineid`,
`common.extversion`, `common.os`, `common.vscodeversion` and
`common.remotename`. VS Code's other common properties are not sent to Scarf.
Scarf matches the IP address an event arrives from to the company or
organisation it belongs to, if any, and to a coarse location.
The raw IP address is discarded once it has been matched.

## What we do not collect

File paths, file names, notebook URIs or titles, Python source code, notebook
cell contents, query text, database contents, GemStone session or cache
names, usernames, email addresses, environment variables, error messages, or
exception text, or your IP address (see "Approximate location" above for
what Azure derives from it before discarding it). Events that record a
exception text, or your IP address (see "Approximate location" and "Scarf"
above for what Azure and Scarf derive from it before discarding it). Events that record a
failure — for example, whether setup or a database start succeeded — carry
only a short classified reason (such as "cancelled" or "failed"), never the
underlying error's own text, because GemStone errors can embed file paths.
Expand All @@ -65,10 +73,10 @@ underlying error's own text, because GemStone errors can embed file paths.

To understand GemDB's usage in aggregate: whether setup and startup succeed
and how long they take, which platforms GemDB runs on and which regions it is
used in, and which features people actually use. That lets us prioritize
fixes and improvements. We do not use it to see what any individual does with
their data or code (see "What we do not collect"), and it is never used for
advertising or profiling. This relies on legitimate interest (GDPR Art.
used in, which organisations use it, and which features people actually use.
That lets us prioritize fixes and improvements. We do not use it to see what
any individual does with their data or code (see "What we do not collect"),
and it is never used for advertising or profiling. This relies on legitimate interest (GDPR Art.
6(1)(f)).

## How it is transmitted and stored
Expand All @@ -78,17 +86,24 @@ Azure subscription owned by GemTalk. They are stored in the **West US 2**
region (United States) and are **retained for 90 days**, after which Azure
deletes them.

Events go to Scarf over TLS too, as web requests to a GemTalk endpoint
Scarf hosts.
<!-- TODO before merge: confirm with Scarf the region its data is stored in
and how long it is retained, and fill in the sentence below. -->
Scarf stores them in **TODO** and retains them for **TODO**.

## Disclosure

Microsoft/Azure processes this data as our infrastructure provider. We do
not sell it, use it for advertising, or share it with any other third party.
Microsoft/Azure processes this data as our infrastructure provider, and
Scarf processes it as our usage analytics provider. We do not sell it, use it
for advertising, or share it with any third party beyond these two.

## Your controls

GemDB honours VS Code's `telemetry.telemetryLevel` setting via
`vscode.env.createTelemetryLogger`:

- `off` — no telemetry is sent.
- `off` — no telemetry is sent, to Azure or to Scarf.
- `crash` / `error` — only events containing an error are sent.
- `all` — all usage events described above are sent.

Expand All @@ -98,9 +113,11 @@ Events are keyed only by `common.vscodemachineid`, so we cannot look up or
delete data for you without that value. To get yours, run **GemDB: Copy
Telemetry ID** from VS Code's Command Palette; it copies the value to your
clipboard. If you send it to the contact address above, we will locate and
delete the matching records.
delete the matching records, in Azure and in Scarf.
<!-- TODO before merge: confirm Scarf can delete events by a
common.vscodemachineid value. -->

## Changes

Last updated: 2026-10-07. Changes to this notice will be published in this
Last updated: 2026-10-15. Changes to this notice will be published in this
repository.
2 changes: 1 addition & 1 deletion __mocks__/@vscode/extension-telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class TelemetryReporter {
private readonly logger: ReturnType<typeof vscode.env.createTelemetryLogger>;

constructor(_connectionString: string) {
// The mock's `createTelemetryLogger` ignores the sender entirely.
// No sender, so the mock's `createTelemetryLogger` records to `__telemetry`.
this.logger = vscode.env.createTelemetryLogger(undefined as never);
}

Expand Down
24 changes: 22 additions & 2 deletions docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,28 @@
What GemDB reports, and when. For what is collected overall, how it is stored,
and how users turn it off, see [USAGE_DATA.md](../USAGE_DATA.md).

Events go to Azure Application Insights and are kept for 90 days. Nothing is
sent when a user sets VS Code's `telemetry.telemetryLevel` to `off`.
Events go to Azure Application Insights, where they are kept for 90 days, and
to Scarf. Nothing is sent to either when a user sets VS Code's
`telemetry.telemetryLevel` to `off`.

Scarf receives the same events and properties, with three differences:

- Only real installs send to it, so everything in Scarf is production data.
Events with an `extensionMode` of `development` or `test` go to Application
Insights alone.
- Every value arrives as a string, measures included: `activationMs` is
`"1234"`, not `1234`.
- Of VS Code's own `common.*` properties it gets only
`common.vscodemachineid`, `common.extversion`, `common.os`,
`common.vscodeversion` and `common.remotename`.

Scarf names the company behind each event from the IP address it arrives
from. In a remote window GemDB runs on the remote machine, so the event comes
from there: over SSH to a cloud machine, or in Codespaces, it reads as the
cloud provider (Amazon, Microsoft, Google) rather than the user's employer.
When counting companies, leave out events whose `common.remotename` is set.
WSL, and a Dev Container on the user's own machine, still send from that
machine, so this undercounts them slightly rather than miscounting.

## The journey the events describe

Expand Down
22 changes: 13 additions & 9 deletions esbuild.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,11 @@ const builds = [
];

/**
* Neither telemetry.ts nor the `@vscode/extension-telemetry` package it wraps
* may reach the shell bundle: there is no extension host there to enforce the
* user's telemetry setting (see telemetry.ts). ESLint's `no-restricted-imports`
* Neither telemetry.ts, scarfTransport.ts, nor the `@vscode/extension-telemetry`
* package may reach the shell bundle: there is no extension host there to
* enforce the user's telemetry setting (see telemetry.ts). scarfTransport.ts
* sends to Scarf with no consent check of its own; the logger telemetry.ts
* wraps it in is what honours the setting. ESLint's `no-restricted-imports`
* catches a direct import of telemetry.ts, but not a rename, a re-export, or a
* facade module — checking the graph esbuild actually built catches all of
* those. The package check is the ultimate guard: it also catches a file that
Expand All @@ -103,13 +105,15 @@ const builds = [
function assertNoTelemetryInShellBundle(result) {
if (!result.metafile) return;
const inputs = Object.keys(result.metafile.inputs);
const reachedOwnModule = inputs.some((input) => input.endsWith('src/telemetry.ts'));
const reachedPackage = inputs.some((input) =>
input.includes('node_modules/@vscode/extension-telemetry/'),
);
if (reachedOwnModule || reachedPackage) {
const reached = [
inputs.some((input) => input.endsWith('src/telemetry.ts')) && 'src/telemetry.ts',
inputs.some((input) => input.endsWith('src/scarfTransport.ts')) && 'src/scarfTransport.ts',
inputs.some((input) => input.includes('node_modules/@vscode/extension-telemetry/')) &&
'@vscode/extension-telemetry',
].find(Boolean);
if (reached) {
throw new BuildAssertionError(
`out/gemdb-shell.js pulled in ${reachedOwnModule ? 'src/telemetry.ts' : '@vscode/extension-telemetry'}. ` +
`out/gemdb-shell.js pulled in ${reached}. ` +
"There is no extension host in the shell to enforce the user's telemetry setting — " +
'this must never ship.',
);
Expand Down
8 changes: 8 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ export default tseslint.config(
'a report* function in src/telemetry.ts and call it from a file ' +
'outside the shell graph (extension.ts, lifecycle.ts, ...).',
},
{
group: ['**/scarfTransport'],
message:
'This file reaches out/gemdb-shell.js, where there is no extension ' +
"host to enforce the user's telemetry setting, and scarfTransport " +
'sends to Scarf with no consent check of its own. Only ' +
'src/telemetry.ts may use it.',
},
],
},
],
Expand Down
17 changes: 17 additions & 0 deletions src/__mocks__/scarfTransport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Stands in for `src/scarfTransport.ts` in every test file, unit and integration, via
* `src/__mocks__/setup.ts`, so no test can send a request to Scarf's real
* gateway — `fakeExtensionContext` is a Production context, which is the
* mode Scarf is sent from. Records each URL instead, for the suites that
* assert on what Scarf would receive.
*/
import type { ScarfTransport } from '../scarfTransport';

/** Every URL sent to Scarf, in order. */
export const __scarfRequests: string[] = [];

export const scarfTransport: ScarfTransport = {
get(url) {
__scarfRequests.push(url);
},
};
9 changes: 9 additions & 0 deletions src/__mocks__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ import { vi } from 'vitest';
// test file, since which files happen to reach telemetry.ts is an
// implementation detail, not something each test should have to know.
vi.mock('@vscode/extension-telemetry');

// Replaces the one module that sends to Scarf with a recorder, in both
// suites. Unlike the line above, this one is about suppressing events:
// the suites that call `initTelemetry` pass a Production context, and
// without it each would send a real request to Scarf. Mocking this module
// rather than `https` keeps the rest of Node's network available to a test
// that needs it; getting the real transport takes a deliberate
// `vi.importActual`, as `scarfTransport.test.ts` does.
vi.mock('../scarfTransport');
54 changes: 47 additions & 7 deletions src/__mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,42 @@ export const ExtensionMode = { Production: 1, Development: 2, Test: 3 } as const
* Replicates VS Code's own merge quirk: when `data.properties` is falsy, its
* real `TelemetryLogger` mixes `common.*` properties into the top level of
* `data` instead of into `data.properties` — which is silently dropped by any
* reader that looks only at `data.properties`. Modelled here with one fake
* common property, so a call that forgets to pass a properties object (see
* reader that looks only at `data.properties`. Modelled here with fake common
* properties, so a call that forgets to pass a properties object (see
* `telemetry.ts`'s own warning against ever doing that) is regression-tested
* rather than only commented on.
* rather than only commented on. `common.vscodemachineid` is there too, as one
* of the common properties Scarf is sent.
*
* Given a real sender — one with `sendEventData` — it hands every event to
* it the way VS Code does: the name prefixed with the extension id, and the
* common properties mixed in by the same rule, plus any
* `additionalCommonProperties` that logger was created with. Only that
* logger gets those, as in VS Code, so a sink that needs a property only
* another logger adds is caught. Common properties win over the event's own
* when a key collides, as VS Code's `mixin` does. Those events never reach
* `__telemetry`, which stays the record of what the `TelemetryReporter` mock
* (which passes no sender) sent, so a second sink is tested through the
* sender's own effects without doubling every event recorded there.
*/
const FAKE_COMMON_PROPERTIES: Record<string, string> = { 'common.fake': 'yes' };
const FAKE_COMMON_PROPERTIES: Record<string, string> = {
'common.fake': 'yes',
'common.vscodemachineid': 'fake-machine-id',
};

/** What VS Code puts in front of every event name a sender receives: `<publisher>.<name>/`. */
export const FAKE_EVENT_NAME_PREFIX = 'gemtalksystems.gemdb/';

interface FakeTelemetrySender {
sendEventData(eventName: string, data?: Record<string, unknown>): void;
}

function isTelemetrySender(sender: unknown): sender is FakeTelemetrySender {
return (
typeof sender === 'object' &&
sender !== null &&
typeof (sender as Partial<FakeTelemetrySender>).sendEventData === 'function'
);
}

/** Two-line insurance: nothing here reads `env` today, but `activate()` does. */
export const env = {
Expand All @@ -123,8 +153,8 @@ export const env = {
},
uiKind: UIKind.Desktop,
createTelemetryLogger(
_sender: unknown,
_options?: unknown,
sender: unknown,
options?: { additionalCommonProperties?: Record<string, unknown> },
): {
isUsageEnabled: boolean;
isErrorsEnabled: boolean;
Expand All @@ -142,9 +172,19 @@ export const env = {
onDidChangeEnableStates: () => new Disposable(() => {}),
logUsage: (name, data) => {
const properties = data?.properties;
const common = { ...options?.additionalCommonProperties, ...FAKE_COMMON_PROPERTIES };
if (isTelemetrySender(sender)) {
sender.sendEventData(
`${FAKE_EVENT_NAME_PREFIX}${name}`,
properties
? { ...data, properties: { ...properties, ...common } }
: { ...data, ...common },
);
return;
}
__telemetry.push({
name,
properties: properties ? { ...FAKE_COMMON_PROPERTIES, ...properties } : {},
properties: properties ? { ...properties, ...common } : {},
measurements: data?.measurements,
});
},
Expand Down
Loading
Loading