Skip to content

feat(plugin-ext): gate plugin telemetry on the telemetry consent provider - #17963

Open
dr14-make wants to merge 1 commit into
eclipse-theia:masterfrom
dr14-make:feat/plugin-telemetry-consent
Open

feat(plugin-ext): gate plugin telemetry on the telemetry consent provider#17963
dr14-make wants to merge 1 commit into
eclipse-theia:masterfrom
dr14-make:feat/plugin-telemetry-consent

Conversation

@dr14-make

@dr14-make dr14-make commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

What it does

Contributes to #17847.

@theia/telemetry (#17809) added TelemetryConsentProvider and the telemetry.telemetryLevel preference, but nothing connects them to the plugin host. TelemetryExtImpl has telemetry hardcoded off, so env.isTelemetryEnabled is always false and every TelemetryLogger.logUsage and logError returns early. Applications collect nothing from plugins, and users have no way to opt in.

This connects the two. The resulting states match VS Code (extHostTelemetry.ts):

telemetry.telemetryLevel env.isTelemetryEnabled logger.isUsageEnabled logger.isErrorsEnabled
off false false false
crash false false false
error false false true
all true true true

Note that env.isTelemetryEnabled is level === 'all', not level !== 'off'.

Three points worth a look during review.

The plugin host now carries a TelemetryLevel instead of a boolean. A boolean cannot express level error, where errors are collected but usage is not. This replaces the _telemetryEnabled field and the updateEnableStates collapse added in #17693, and rewrites that PR's spec, which tested the boolean behaviour. Levels are compared with isKindAllowedByLevel from @theia/telemetry so the crash/error/all ordering stays in one place. logUsage and logError check both the level and the per-kind flag, so a plugin that sets isUsageEnabled on its own logger still cannot send anything the user did not consent to.

The first level is passed in $init, not pushed over RPC. Plugins create their loggers while they activate, and createAPIFactory runs before $init, so a level that only arrived over RPC would drop everything logged during activation. PluginManagerInitializeParams gets an optional telemetryLevel (absent means off, which is what hosts that do not set it get today), applied in $init next to the existing envExt.set* calls. RPC then only carries later changes. To apply it there the plugin manager needs the telemetry ext, so TelemetryExtImpl is bound in both plugin host containers and passed to createAPIFactory, the same way EnvExtImpl is.

The frontend reads the level from TelemetryConsentProvider, not from params.preferences, because the provider is meant to be rebindable.

That means @theia/plugin-ext now depends on @theia/telemetry. All four example apps already pull it in, so it resolves today, but plugin-ext does need the binding now, and an application that ships plugin-ext without telemetry would fail in container.get. If the dependency is a problem, the alternative is an isBound guard that defaults to off. Happy to switch.

TelemetryLogger also takes the level-change Event and owns the subscription, so a disposed logger no longer stays referenced by the emitter for the lifetime of the plugin host. That leak already existed, but the constructor changes here anyway, so fixing it now avoids a second breaking change later.

Also fixes a stray paren in the TELEMETRY_EXT proxy identifier string ('TelemetryExt)'). It is only used as a key, so it does no harm today.

Two things that are not visible in the diff:

  • The level passed in $init is current only because startPlugins awaits theiaReadyPromise, which includes preferenceServiceImpl.ready, the same promise PreferenceTelemetryConsentProvider uses to set its level. That spans three files and createTheiaReadyPromise is protected, so an override that drops preferenceServiceImpl.ready would bring back the dropped activation events. A rebound TelemetryConsentProvider that reads consent from a file or over the network has no such barrier either and would report off at that point. If that is a case we want to support, TelemetryConsentProvider probably needs a readiness promise. I am happy to follow up separately.
  • package-lock.json has one hand-added line for the new workspace dependency. The regeneration script in doc/lockfile-maintenance.md needs Node 24, and regenerating on an older npm strips libc fields and platform-specific optional entries. For a workspace-internal dependency with no install scripts and no platform binaries the result is the same, and verify-lockfile-platforms.js passes locally. Let me know if you would rather have it regenerated.

The new API is marked @experimental, like everything added in #17809.

This change was generated by AI, then validated and reviewed by a human. I have read, reviewed and tested all of it myself, and I am accountable for it as the author.

How to test

npx lerna run test --scope @theia/plugin-ext. telemetry-ext.spec.ts covers every row of the table above, the state transitions, logger disposal, and a logger created while the level forbids usage. telemetry-main.spec.ts covers pushing level changes to the plugin host.

By hand, with a plugin that calls logUsage and logError from its activate():

  1. Set "telemetry.telemetryLevel": "all" and start the application. The events logged during activation reach the sender, and env.isTelemetryEnabled is true inside activate().
  2. Restart with off, the default. Nothing reaches the sender.
  3. Restart with error. logUsage is dropped, logError is delivered.
  4. Change the preference while the application is running. onDidChangeEnableStates fires and the logger flags follow. env.onDidChangeTelemetryEnabled only fires when the value it reports changes, so crash to error does not fire it.

I ran all four steps in examples/browser against both plugin hosts: the Node host with a VS Code plugin, and the web worker host with a Theia frontend plugin.

Follow-ups

The rest of #17847: forwarding plugin telemetry into Theia's own topics and sinks. Also out of scope here:

  • TelemetryLogger.getCommonProperties() returns [], so there is no common.* injection.
  • env.machineId and env.sessionId are new UUIDs on every host start, so machineId is not stable across restarts.

Breaking changes

  • This PR introduces breaking changes and requires careful review. If yes, the breaking changes section in the changelog has been updated.

Review checklist

Reminder for reviewers

@github-project-automation github-project-automation Bot moved this to Waiting on reviewers in PR Backlog Aug 27, 2026
@dr14-make
dr14-make force-pushed the feat/plugin-telemetry-consent branch from 51b77ba to 1a864bf Compare August 27, 2026 14:11
@dr14-make

Copy link
Copy Markdown
Contributor Author

I have verified this end to end, including against a real telemetry backend, so I can confirm the gate works rather than only that the tests pass.

In examples/browser I used a small plugin that calls logUsage and logError from its activate(), and checked all four levels in both plugin hosts. The Node host and the web worker host agree:

level env.isTelemetryEnabled usage delivered errors delivered
off false no no
crash false no no
error false no yes
all true yes yes

Changing the preference while the application runs also works: onDidChangeEnableStates fires and the logger flags follow, and env.onDidChangeTelemetryEnabled only fires when the value it reports actually changes, so crash to error does not fire it.

I then tried it in a downstream Theia application that bundles an extension routing all of its analytics through vscode.env.createTelemetryLogger. With the level set to all, the event that extension logs during activation reached its telemetry backend and was ingested. With the level set to off or error the same call was dropped at the gate and nothing was sent.

That downstream case is the one I care most about: the extension builds its logger and logs its first event on consecutive statements of activate(), so it only works because the level is passed in $init rather than pushed over RPC afterwards. An RPC-only design dropped that event on every start.

Worth flagging for whoever reviews the @theia/plugin-ext to @theia/telemetry dependency: HostedPluginSupport injects TelemetryConsentProvider and is resolved during FrontendApplicationContribution.onStart, so in an application where the binding is missing the failure is an Inversify error at startup rather than telemetry silently staying off. In an application that has @theia/telemetry this cannot happen, and I would rather it fail loudly than quietly, but it is a reason to be deliberate about the dependency either way.

…ider

`@theia/telemetry` shipped `TelemetryConsentProvider` and the
`telemetry.telemetryLevel` preference, but nothing connected them to the plugin
host. `TelemetryExtImpl` had telemetry hardcoded off, so `env.isTelemetryEnabled`
was always false and every `TelemetryLogger.logUsage` and `logError` returned
early. Applications collected nothing from plugins and users could not opt in.

The plugin host now carries a `TelemetryLevel` instead of a boolean. A boolean
cannot express level `error`, where errors are collected but usage is not, so
carrying one made that level impossible to represent. Levels are compared with
`isKindAllowedByLevel` so the ordering stays in one place.

Plugins create their loggers while they activate, so a level that only arrived
over RPC would drop everything logged during activation. The level is passed in
`PluginManagerInitializeParams` and applied in `$init` before any plugin
activates; RPC then only carries later changes. To apply it there, the plugin
manager needs the telemetry ext, so `TelemetryExtImpl` is bound in both plugin
host containers and passed to `createAPIFactory` like `EnvExtImpl` is.

The frontend reads the level from `TelemetryConsentProvider` rather than from
`params.preferences`, because the provider is meant to be rebindable.

Contributes to eclipse-theia#17847

Signed-off-by: Dmitrij Rozdestvensky <dmitrij.rozdestvensky@juliahub.com>
@dr14-make
dr14-make force-pushed the feat/plugin-telemetry-consent branch from 1a864bf to 29e32c5 Compare August 27, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Waiting on reviewers

Development

Successfully merging this pull request may close these issues.

1 participant