Skip to content

feature: route LicenseService heartbeat through TelemetryPort adapter#89

Closed
jamby77 wants to merge 24 commits intomasterfrom
feature/79-license-heartbeat-telemetry-adapter
Closed

feature: route LicenseService heartbeat through TelemetryPort adapter#89
jamby77 wants to merge 24 commits intomasterfrom
feature/79-license-heartbeat-telemetry-adapter

Conversation

@jamby77
Copy link
Copy Markdown
Collaborator

@jamby77 jamby77 commented Apr 2, 2026

Summary

  • Inject TELEMETRY_CLIENT into LicenseService as @Optional() and delegate telemetry_ping heartbeat events through the TelemetryPort adapter via capture() instead of direct HTTP
  • Version info (latestVersion/releaseUrl) continues to work via periodic validateLicense() calls to the entitlement URL
  • sendStartupError() remains unchanged — direct HTTP, bypasses telemetry opt-out
  • Export TELEMETRY_CLIENT token from TelemetryModule so other modules can inject it

Test plan

  • Heartbeat delegates to adapter via capture()
  • Adapter not called when BETTERDB_TELEMETRY=false
  • Graceful handling when telemetry client is not injected (@Optional())
  • Version info stored from entitlement response via validateLicense()
  • Existing license service tests pass (33 total)
  • pnpm build passes

Closes #79


Note

Medium Risk
Introduces new telemetry providers (PostHog/HTTP/noop) across API and web and reroutes license heartbeat and frontend event tracking through these clients, which could affect outbound telemetry behavior and startup gating if misconfigured. Risk is mitigated by opt-out handling, noop fallbacks, and added unit/integration tests.

Overview
Adds a pluggable telemetry layer end-to-end. The API now defines a TelemetryPort, provides noop/http (stub)/posthog adapters plus a TelemetryClientFactory, exposes GET /telemetry/config, and injects a TELEMETRY_CLIENT into TelemetryModule with graceful shutdown.

Reworks telemetry emission to go through the injected client: UsageTelemetryService switches from direct HTTP fetch to telemetryClient.capture()/identify(), and LicenseService routes its periodic telemetry_ping heartbeat via the optional TELEMETRY_CLIENT while keeping version checks via periodic validateLicense().

On the web app, introduces useTelemetry and telemetry clients (ApiTelemetryClient posting to /telemetry/event, and PosthogTelemetryClient via posthog-js), updates idle/navigation tracking to call client.capture(), and blocks ServerStartupGuard until telemetry config fetch resolves. Environment/config updates add telemetry-related vars and validation, and extensive new tests cover provider selection, config endpoint behavior, and adapter delegation.

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

jamby77 added 24 commits April 2, 2026 09:29
…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
…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.
- Guard sendEvent against empty instanceId (no licenseService case)
- Fix payload spread order: base properties override caller payload
- Use NestJS Logger instead of console.warn in factory and module
- Add PostHog stub warning when API key is set but adapter not implemented
- Catch shutdown errors in onModuleDestroy (non-fatal teardown)
- Handle both boolean and string 'false' for BETTERDB_TELEMETRY
- Warn and fall back to noop when ENTITLEMENT_URL path is invalid
- Add factory tests for http, posthog, unknown provider, and URL fallback
- Restructure integration tests: guard behavior + identity lifecycle
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
…iguration

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
…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.
… 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

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
…#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.
@jamby77 jamby77 closed this Apr 2, 2026
@github-actions github-actions bot locked and limited conversation to collaborators Apr 2, 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.

Route LicenseService heartbeat telemetry through TelemetryPort adapter

1 participant