Skip to content

feature: telemetry adapter abstraction (TelemetryPort + NoopAdapter + factory)#77

Merged
KIvanow merged 19 commits intomasterfrom
feature/71-telemetry
Apr 3, 2026
Merged

feature: telemetry adapter abstraction (TelemetryPort + NoopAdapter + factory)#77
KIvanow merged 19 commits intomasterfrom
feature/71-telemetry

Conversation

@jamby77
Copy link
Copy Markdown
Collaborator

@jamby77 jamby77 commented Apr 2, 2026

Summary

  • Introduces TelemetryPort interface with capture(), identify(), shutdown() — matching the StoragePort pattern
  • NoopTelemetryAdapter for opt-out and misconfiguration fallback
  • TelemetryClientFactory reads TELEMETRY_PROVIDER and BETTERDB_TELEMETRY from NestJS ConfigService
  • Refactors UsageTelemetryService to inject TELEMETRY_CLIENT token instead of owning HTTP logic
  • TelemetryModule wires factory via useFactory provider, calls shutdown() on destroy
  • Adds TELEMETRY_PROVIDER, POSTHOG_API_KEY, POSTHOG_HOST to env schema
  • BETTERDB_TELEMETRY=false → factory returns NoopAdapter regardless of provider
  • Misconfiguration logs warning and falls back to NoopAdapter (never crashes)

Test plan

  • Unit tests for NoopTelemetryAdapter (3 tests)
  • Unit tests for TelemetryClientFactory (4 tests — default, noop, opt-out, missing key)
  • Integration test: NestJS DI wiring, service → adapter delegation (2 tests)
  • pnpm build passes
  • Manual: app boots normally with no telemetry env vars set

Closes #71


Note

Medium Risk
Adds new telemetry plumbing across backend and frontend, including provider selection and build/runtime env injection, which could affect app startup behavior and outbound network calls if misconfigured. Safeguards and fallbacks reduce failure risk but this still touches core initialization paths.

Overview
Introduces a telemetry adapter abstraction end-to-end. The API now defines a TelemetryPort and wires a global TELEMETRY_CLIENT via TelemetryClientFactory, supporting posthog, legacy http, and noop providers with opt-out via BETTERDB_TELEMETRY.

Backend telemetry is refactored to use the injected client. UsageTelemetryService no longer posts directly; it identifies/captures through the adapter and the module shuts the client down on destroy. LicenseService heartbeat telemetry is rerouted through the same adapter, while version info is now sourced from entitlement validation.

Frontend telemetry is added with PostHog support. A new useTelemetry hook fetches GET /telemetry/config and selects either an API-backed client or a PostHog JS client; existing trackers now call client.capture(), and startup gating waits for telemetry config readiness.

Config/build updates. Adds telemetry env vars to .env.example and API env schema, bakes frontend PostHog vars via Docker build args, injects backend PostHog defaults post-build, and adds posthog-node/posthog-js dependencies plus comprehensive unit/integration tests.

Written by Cursor Bugbot for commit 7be67a4. This will update automatically on new commits. Configure here.

…r, and factory

Introduce provider-agnostic telemetry pattern matching the existing
StoragePort/StorageClientFactory architecture. UsageTelemetryService now
delegates to an injected TELEMETRY_CLIENT token instead of owning HTTP
logic directly. Factory reads config from NestJS ConfigService and
returns NoopAdapter when telemetry is disabled or misconfigured.

Closes #71
Comment thread apps/api/src/telemetry/usage-telemetry.service.ts
Comment thread apps/api/src/telemetry/usage-telemetry.service.ts
Comment thread apps/api/src/telemetry/usage-telemetry.service.ts Outdated
Comment thread apps/api/src/telemetry/__tests__/telemetry-integration.spec.ts
Comment thread apps/api/src/telemetry/telemetry-client.factory.ts Outdated
Comment thread apps/api/src/config/env.schema.ts Outdated
@claude

This comment was marked as outdated.

Comment thread apps/api/src/config/env.schema.ts
Comment thread apps/api/src/telemetry/telemetry-client.factory.ts Outdated
Comment thread apps/api/src/telemetry/usage-telemetry.service.ts Outdated
Comment thread apps/api/src/telemetry/usage-telemetry.service.ts
Comment thread apps/api/src/telemetry/__tests__/telemetry-client.factory.spec.ts
Comment thread apps/api/src/telemetry/telemetry.module.ts
@claude

This comment was marked as outdated.

jamby77 added 2 commits April 2, 2026 09:43
…orrect types

Add HttpTelemetryClientAdapter and PosthogTelemetryClientAdapter stubs
implementing TelemetryPort. Factory now returns the correct adapter type
per provider config. Real implementations in #72 and #73.
Comment thread apps/api/src/config/env.schema.ts
Comment thread apps/api/src/telemetry/usage-telemetry.service.ts
Comment thread apps/api/src/telemetry/usage-telemetry.service.ts
Comment thread apps/api/src/telemetry/telemetry-client.factory.ts Outdated
Comment thread apps/api/src/config/env.schema.ts
Comment thread apps/api/src/telemetry/telemetry-client.factory.ts
Comment thread apps/api/src/config/env.schema.ts
Comment thread apps/api/src/telemetry/telemetry.module.ts Outdated
Comment thread apps/api/src/telemetry/telemetry-client.factory.ts Outdated
Comment thread apps/api/src/telemetry/usage-telemetry.service.ts
Comment thread apps/api/src/telemetry/usage-telemetry.service.ts
Comment thread apps/api/src/telemetry/usage-telemetry.service.ts Outdated
Comment thread apps/api/src/telemetry/telemetry-client.factory.ts Outdated
Comment thread apps/api/src/telemetry/telemetry-client.factory.ts Outdated
@claude

This comment was marked as outdated.

@KIvanow
Copy link
Copy Markdown
Member

KIvanow commented Apr 2, 2026

@cursor review

Comment thread apps/api/src/telemetry/usage-telemetry.service.ts Outdated
Comment thread apps/api/src/telemetry/telemetry-client.factory.ts Outdated
feature: implement PosthogTelemetryClientAdapter with posthog-node

Thin wrapper around posthog-node: capture(), identify(), and shutdown()
delegate directly to the PostHog client. Remove stub warning from
factory since adapter is now real.

Closes #73
Comment thread apps/api/src/telemetry/telemetry-client.factory.ts
* feature: implement HttpTelemetryClientAdapter with fetch transport

Replace stub with real HTTP adapter: fire-and-forget POST to telemetry
URL with 5s timeout, silent error handling. identify() and shutdown()
are no-ops.

Closes #72

* fix: track in-flight requests, abort on shutdown, improve error test

- capture() tracks pending AbortControllers, clears on completion
- shutdown() aborts all in-flight requests
- Error swallowing test now drains microtask queue for proper assertion

* fix: clear timers directly on shutdown to prevent delayed process exit

* chore: allow underscore-prefixed unused vars in eslint config

Add argsIgnorePattern and varsIgnorePattern for _ prefix to
@typescript-eslint/no-unused-vars. Fixes lint errors on interface
implementations with intentionally unused parameters.
Comment thread apps/api/src/telemetry/usage-telemetry.service.ts Outdated
…ation (#84)

* feature: add GET /telemetry/config endpoint for frontend runtime configuration

Returns instanceId, telemetryEnabled, provider, and optional posthog
fields. Frontend uses this at runtime to initialize the correct
telemetry client without build-time env vars.

Closes #74

* fix: handle both boolean and string 'false' for BETTERDB_TELEMETRY in config endpoint

* refactor: improve readability and type clarity in TelemetryController

* feature: extract telemetry event types to shared, add DTO with class-validator

Move frontend/backend telemetry event constants and types to
@betterdb/shared for frontend type safety. Add TelemetryEventDto
with class-validator @isin and @isObject for NestJS validation.
Refactor controller to use DTO, switch statement, and private handlers.

* fix: remove posthog API key and host from telemetry config endpoint

* fix: add default case to event switch to throw on unhandled event types

* feature: frontend TelemetryConfigProvider + ApiTelemetryClient + hook refactor (#85)

* feature: frontend TelemetryConfigProvider, ApiTelemetryClient, and hook refactor

Add TelemetryClient interface, NoopTelemetryClient, ApiTelemetryClient.
TelemetryConfigProvider fetches GET /telemetry/config on mount, selects
the correct client, and exposes it via useTelemetry() context hook.
Falls back to ApiTelemetryClient on config fetch failure.

Refactor useNavigationTracker and useIdleTracker to use useTelemetry()
instead of direct fetchApi calls. useConnection telemetry left as-is
since it creates context at a level above the provider.

Closes #75

* fix: remove nonexistent 'api' provider case, use 'http' consistently

* chore: move useTelemetry hook to hooks/ directory

* chore: replace TelemetryConfigProvider with singleton useTelemetry hook

Remove the context provider — config loading now lives in useTelemetry
hook with a module-level singleton. Config is fetched once, cached, and
shared across all consumers. Hook returns { client, ready }.
No provider wrapping needed in App.

* chore: simplify useTelemetry to async function with module-level promise

* feature: block app render until telemetry client is ready in ServerStartupGuard

* chore: use TanStack Query for telemetry config fetching in useTelemetry

* chore: use 30min stale time and default retry for telemetry config query

* feature: frontend PosthogTelemetryClient with posthog-js (#88)

* feature: frontend PosthogTelemetryClient with posthog-js

Thin wrapper around posthog-js: capture() maps page_view to native
$pageview, identify() and shutdown() delegate directly. Wired into
useTelemetry hook — activated when backend returns provider=posthog
and VITE_POSTHOG_API_KEY is set at build time. Falls back to
ApiTelemetryClient when key is missing.

Closes #76

* chore: add telemetry env vars to .env.example

* chore: add frontend telemetry env vars to .env.example

* fix: set Vite envDir to monorepo root so .env vars are loaded

* refactor: update PostHog client to use instance-based API, adjust env vars and tests

Switch from the global `posthog` instance to an instance-based approach with `PostHog`. Updated env vars for clarity (`VITE_POSTHOG_*` to `VITE_PUBLIC_POSTHOG_*`). Refactored `useTelemetry` to manage lifecycle via `useEffect` and updated tests to mock the new client structure.

* fix: store posthog.init() instance, use || for empty string host fallback

- Use returned PostHog instance from init() instead of global
- Use || instead of ?? so empty string host falls back to default
- Remove debug console.log

* fix: use module-level singleton for telemetry client to prevent per-component reset

* chore: remove unused backend telemetry type exports from shared package

* chore: remove NoopTelemetryClient from frontend, use ApiTelemetryClient as fallback

* fix: call identify with instanceId when creating PostHog frontend client
Comment thread apps/web/src/components/ServerStartupGuard.tsx
…#90)

* feature: route LicenseService heartbeat through TelemetryPort adapter (#79)

Inject TELEMETRY_CLIENT into LicenseService as @optional() and delegate
heartbeat telemetry_ping events through the adapter via capture() instead
of direct HTTP. Version info continues via validateLicense() calls to
the entitlement URL. sendStartupError() remains unchanged.

* fix: make TelemetryModule @global() so TELEMETRY_CLIENT is injectable across modules
@jamby77 jamby77 requested a review from KIvanow April 2, 2026 16:23
Comment thread apps/web/src/telemetry/clients/posthog-telemetry-client.ts Outdated
# Conflicts:
#	apps/web/package.json
#	pnpm-lock.yaml
Comment thread apps/web/src/hooks/useTelemetry.ts
- Update default PostHog host to EU endpoint.
- Wrap `identify` calls in try/catch to prevent crashes.
- Support multiple false-like values for `BETTERDB_TELEMETRY` config.
- Enhance tests to cover extended false-like value handling.
Comment thread apps/api/src/config/env.schema.ts
Comment thread proprietary/licenses/license.service.ts
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.


for (const [placeholder, value] of Object.entries(replacements)) {
if (value && source.includes(placeholder)) {
source = source.replace(placeholder, value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Script replaces only first placeholder occurrence

Low Severity

source.replace(placeholder, value) only replaces the first occurrence of each placeholder token. If the compiled output ever contains the same placeholder more than once (e.g., due to bundler inlining or source-map references), subsequent occurrences will remain as literal __BETTERDB_POSTHOG_API_KEY__ strings at runtime, causing the factory's startsWith('__') guard to treat the key as unset. Using replaceAll (or a global regex) would be more robust.

Fix in Cursor Fix in Web

@KIvanow KIvanow merged commit 7d679d9 into master Apr 3, 2026
3 checks passed
@KIvanow KIvanow deleted the feature/71-telemetry branch April 3, 2026 13:09
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telemetry adapter abstraction: TelemetryPort + NoopAdapter + factory

2 participants