Narrows this PR to OSS-566 + OSS-568. The OSS-565 CI-suppression effort is
backed out entirely:
- the 10 workflows' `COPILOTKIT_TELEMETRY_DISABLED` / `DO_NOT_TRACK` env and the
egress-block steps
- `examples/e2e/playwright.config.ts` env defaults
- the runtime/shared test-env manipulation and the `telemetry-opt-out-env`
helper
- the `navigator.webdriver` check and its tests
Every reverted path is byte-identical to the merge-base — verified rather than
assumed, since `git checkout origin/main -- <path>` would otherwise have
imported main's newer content for files that had moved on.
One piece is deliberately kept, because it is not about CI: the jsdom egress
guard. These tests run with a real `fetch`, and the inspector's telemetry is
fire-and-forget, so any test touching a banner / threads / open path without
stubbing fetch POSTs a real `oss.inspector.*` event to the live sink — from
developer machines as well as CI. The announcement-dismissal tests did exactly
that before the guard, and the new `opened` / `banner_dismissed` tests hit the
same send path, so shipping the events without it would make our own dataset
dirtier. Its comments no longer cite OSS-565; the rationale stands on its own.
What remains: `oss.inspector.opened` with open-source attribution, the
`banner_viewed` surface split, and first-class `banner_dismissed`. No inspector
telemetry waits on the /info handshake.
Verified: web-inspector 112, runtime 1,760, shared 199 (the last two now on
main's own test files). tsc clean, oxfmt clean, oxlint 9 warnings all
pre-existing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the /info-coupled approach with two mechanisms that need no handshake,
and backs the coupling out entirely.
The problem with gating on /info: `telemetryDisabled` only reaches the browser
through an async handshake, so either events race it (the hole) or they wait for
it (fail-closed). Waiting means an inspector whose runtime never connects — or
whose /info 404s, which sets status to `Error`, not `Connected` — goes silent for
the whole session. That penalizes real users with broken setups, who are exactly
the people most likely to have the inspector open.
Instead:
1. `navigator.webdriver` in `track()`. Spec'd, synchronous, available on the
first line of script, and true under every Playwright launch — verified
against this repo's Playwright 1.59.1 in both headless and headed mode. Needs
no per-app env plumbing and covers pages served by deployed environments,
which no CI-side variable could reach. Suppresses only on an explicit `true`:
absent/falsy means "not automation", never "unknown, so stay quiet".
2. A job-wide egress block (`.github/scripts/block-telemetry-egress.sh`) pointing
the sink at loopback for both address families, added to the 12 CI jobs that
run or boot real CopilotKit code. Backstop for anything that reads neither the
env nor the DOM. The frozen integration container gets the same via
`--add-host`, since neither the workflow env nor the runner's hosts file
crosses that boundary.
Backed out of the inspector: the egress gate and held queue, `pendingOpened`,
and `pendingBannerViewed` (the last pre-dating this PR). Inspector telemetry no
longer references the handshake at all — `index.ts` is net −94 lines and the
opt-out is now a single boolean check per call site, as it reads.
Two bugs this surfaced, both caught by existing or new tests rather than
shipped: removing the flush step also removed the only `telemetryDisabled` check
on the `banner_viewed` path, and a thread-refetch test counted calls on a stubbed
global `fetch`, so telemetry that now sends immediately inflated it — scoped to
the thread URLs it actually means.
Also corrects the workflow comments, which credited the /info chain for
browser-side suppression it never reliably provided.
Verified: web-inspector 116, runtime 1,760, shared 199. tsc clean, oxfmt clean,
oxlint back to the baseline 8 warnings (fixed a dead type import and a
non-capturing test helper I introduced). actionlint 344 findings before and
after, byte-identical modulo shifted line numbers. shellcheck clean. tsdown
build ships the check in dist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit closed the pre-handshake hole for the three events this PR
touches, but every other inspector event still posted on the placeholder.
`AgentRegistry._telemetryDisabled` starts `false`, so the per-call-site
`if (this.core?.telemetryDisabled) return` check passes while disconnected and
the event goes out — reproduced with the threads tab, which emitted
`threads_tab_clicked` before /info could report the opt-out. 15 call sites
shared the flaw (threads tab, memories tab, view-state, CTAs, example tour).
Rather than queue at each of them, the gate now lives in `telemetry.ts` at the
single egress boundary: `track()` holds events while the runtime decision is
unknown, and the inspector calls `resolveTelemetryGate("allowed" | "denied")`
once the handshake lands. Nothing reaches the network without an explicit
allow.
That makes the per-event queues added in e7e8922 redundant, so
`pendingBannerViewed` and `pendingBannerInteractions` are gone (net −101 lines
in index.ts). `pendingOpened` stays: it exists to resolve `license_status` /
`runtime_mode` / `runtime_url_type` at flush time, which is a payload concern
rather than a deferral one. The gate resets on `detachFromCore` so a later core
starts from "unknown" instead of inheriting the previous runtime's decision.
Two fidelity details the refactor forced, both covered: held events carry the
timestamp of the interaction rather than of the release, and the opt-out is
re-checked at release in case the user opts out mid-handshake.
Trade worth stating: an inspector whose runtime never connects now emits
nothing. Consent cannot be established without the handshake, so silence is
the correct default.
Coverage: threads tab held then dropped on disabled / sent on allowed, plus a
gate unit suite (unknown holds, allowed releases in order, denied discards,
post-denial drops, queue bound, reset, opt-out precedence, event-time ts).
web-inspector 127 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Importing the factory from vitest.setup.ts tripped TS6059 (file outside
rootDir) in the package's typecheck. The helper now lives in
src/lib/testing/ — test-only, never referenced by the bundle entry, and
confirmed absent from dist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
OSS-566: there was no event recording that the Inspector panel was opened.
Opens could only be inferred from in-panel activity (a floor) or from
`banner_clicked` cta=body, which misses the floating-button path entirely.
Adds `oss.inspector.opened` with an `open_source` property, queued behind
the runtime handshake so an open is never reported for a runtime that has
telemetry disabled. Restoring a persisted-open panel deliberately does not
count — otherwise every reload / dev-server hot reload would be an "open".
OSS-568: `banner_viewed` fired once at fetch time and could not say which
of the two announcement surfaces the user actually saw — the bubble on the
collapsed widget or the card inside the opened panel. The event now carries
a `surface` stamped at fire time and dedups per (banner, surface), so
opening the panel records the card impression as its own signal. Dismissal
is also promoted to a first-class `oss.inspector.banner_dismissed` event,
emitted alongside the existing `banner_clicked { cta: "dismiss" }` so
dashboards reading the `cta` value keep working.
Both events pass the sink's `oss.inspector.` prefix gate, so no
telemetry-sink deploy is required.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Repairs TypeScript check-types across the monorepo and adds a CI gate so
regressions are caught going forward:
- core: bundler module resolution and strict-mode fixes
- sdk-js: bundler module resolution; keep codegen, formatter, packaging working
- react-core: fixes across components, hooks, and tests
- react-native: restore catch binding referenced by TypeError cause
- runtime: repair check-types and bound AI SDK schema inference
- web-inspector: nodenext import extensions, export Anchor
- remaining packages and node example: assorted check-types repairs
- deps: add missing type-only devDependencies
- license context driven from /info licenseStatus
- ci: run check-types in the static quality workflow
Squashed from 12 commits for a single, easily-revertable change.
- Defer trackBannerViewed until runtime connection is established via
pendingBannerViewed + flushPendingBannerViewed(), preventing the race
where the CDN response beats the /info handshake and fires the event
before core.telemetryDisabled is known
- Remove dead isTelemetryOptedOut() short-circuit from track(); opt-out
is enforced at call sites via core.telemetryDisabled, not localStorage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Don't fire banner_clicked on copy-button clicks (move track call to
the non-copy-button branch of handleAnnouncementContentClick)
- Defer ensureTelemetryDistinctId() to runtime connection so no UUID
is written to localStorage when COPILOTKIT_TELEMETRY_DISABLED is set
- Remove setTelemetryOptOut re-export from telemetry.ts (no production
caller; tests already import directly from persistence.ts)
- Fix docs: replace Privacy tab toggle description with env var opt-out
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per Ben Taylor: the ingest lambda parses the X-CopilotKit-Telemetry-Id header
as telemetry_id for the distinct ID. Send it as a header in addition to keeping
it in the POST body properties.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move `package` from properties string to top-level `{ name }` object per
Ben's confirmed IngestPayload schema (telemetry-sink-ingest/index.ts:127-134)
- Add typed per-event helpers trackBannerViewed/trackBannerClicked/trackThreadsTabClicked
to enforce property shapes at call sites and prevent PII leakage under wrong keys
- Add trackBannerClickedOnce guard in index.ts (per-mount Set keyed by
banner_id + cta) to prevent banner_clicked inflation on repeated clicks
- Fix handleTelemetryOptOutToggle: replace ?? true fallback with
instanceof HTMLInputElement guard (wrong fallback was a privacy bug)
- Add threadsTabClicked re-selection guard (skip if already on threads tab)
- Replace getTelemetryDistinctIdForUrl() call on mount with ensureTelemetryDistinctId()
- Add inMemoryFallbackId in persistence.ts for funnel coherence when
localStorage is unavailable (same UUID returned per page load)
- Add _resetTelemetryPersistenceForTesting() for test isolation
- Remove @copilotkit/shared dep from telemetry-disclosure.ts (inline
env-var check; keeps module self-contained and testable in isolation)
- Add clearMocks: true to web-inspector vitest config (fixes spy call
history accumulating across tests)
- Expand telemetry.test.ts to 22 tests covering wire body shape, opt-out,
5 error-resilience paths, typed helpers, distinct ID lifecycle (SSR +
localStorage-throws + funnel coherence), maybeShowDisclosure, and
getTelemetryDistinctIdForUrl
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three V1 funnel events from the inspector — oss.inspector.banner_viewed,
oss.inspector.banner_clicked, oss.inspector.threads_tab_clicked — plus a
privacy panel for opt-out, a first-run console disclosure on inspector
mount and runtime startup, and inspector content added to the canonical
/telemetry docs page on main.
Inspector POSTs directly from the browser to telemetry.copilotkit.ai/ingest
(per ticket: URL is intentionally clearly named for transparency in DevTools).
Inline fetch POST in lib/telemetry.ts — no @copilotkit/shared dep on the
inspector, no dependency on any non-main branch.
Wire body shape (conservative; needs Ben confirmation):
POST https://telemetry.copilotkit.ai/ingest
{ event, properties: { ...caller, distinct_id, package }, ts }
If the lambda expects a richer envelope, update the single JSON.stringify
in lib/telemetry.ts.
Privacy invariants:
- Opt-out toggle short-circuits before any network call (verified by test).
- Properties are scoped to event metadata only — no message content, agent
state, prompts, completions, banner markdown. Negative test pins the wire.
- Anonymous distinct ID (UUID v4 in localStorage) set on inspector load and
propagated onto banner CTA links as ?posthog_distinct_id=<uuid> so the
destination site can posthog.alias() and close the
banner_viewed → banner_clicked → signup_attributed funnel. URL param
suppressed when opted out.
- Console disclosure on first inspector mount and runtime startup. Both
link to https://docs.copilotkit.ai/telemetry.
Plan gaps addressed:
- CTA name on banner_clicked: cta:'body'|'dismiss' (click location) plus
optional cta_label read defensively. Sam: confirm dismiss treatment.
- De-anon opt-out folded into the single toggle. Docs say so explicitly.
- banner_viewed dedup: per-instance Set<string> keyed by timestamp.
- EPIC consent / pixel review: out of scope for this PR; flagged at merge.
Deferred for V1.1:
- Wire body shape (Ben).
- Event-type allowlist for oss.inspector.* (Ben — oss-path-to-production).
- posthog_distinct_id URL-param key name (Ben/Tyler/website team).
Refs https://linear.app/copilotkit/issue/OSS-96
Flatten all packages from packages/v1/* and packages/v2/* into packages/* —
every package now lives directly under the @copilotkit/ scope with no v1/v2
subdirectories.
- Move all v1 packages (react-core, react-ui, runtime, shared, etc.) from
packages/v1/* to packages/*
- Absorb v2 react code into packages/react-core/src/v2/ (exported via /v2 subpath)
- Absorb v2 agent code into packages/runtime/src/agent/ (exported via /v2 subpath)
- Move v2 packages (core, angular, demo-agents, etc.) to packages/*
- Replace all @copilotkitnext/* imports with @copilotkit/* equivalents
- Keep @copilotkitnext/angular as the sole exception (angular remains on next)
- Update CI workflows, renovate config, release scripts for flat structure
- No public API surface changes — all exports fields are preserved
Co-authored-by: Alem Tuzlak <t.zlak@hotmail.com>
Signed-off-by: Tyler Slaton <tyler@copilotkit.ai>