feat(obs): add wrapper telemetry foundations (FER-10253)#14
Conversation
a3a904e to
d8dc2d6
Compare
|
From Claude: ReviewOverall this is a well-structured PR — clean module boundaries, good no-op-by-default story, solid test coverage on the lifecycle paths. A few items to address before merge, plus some non-blocking follow-ups. Should fix before merge1.
|
| github_run_url: getGithubRunUrl(), | ||
| org: extractOrg(repository), | ||
| config_repo: repository, | ||
| config_commit_sha: env.FERN_CONFIG_COMMIT_SHA ?? env.GITHUB_SHA ?? "", |
There was a problem hiding this comment.
should we fallback to undefined instead?
| */ | ||
| export function getAutomationContext(): AutomationContext { | ||
| const env = process.env; | ||
| const repository = env.FERN_CONFIG_REPO ?? env.GITHUB_REPOSITORY ?? ""; |
There was a problem hiding this comment.
should we fallback to undefined instead?
| org: extractOrg(repository), | ||
| config_repo: repository, | ||
| config_commit_sha: env.FERN_CONFIG_COMMIT_SHA ?? env.GITHUB_SHA ?? "", | ||
| config_branch: env.FERN_CONFIG_BRANCH ?? env.GITHUB_HEAD_REF ?? env.GITHUB_REF_NAME ?? "", |
There was a problem hiding this comment.
should we fallback to undefined instead?
| config_commit_sha: env.FERN_CONFIG_COMMIT_SHA ?? env.GITHUB_SHA ?? "", | ||
| config_branch: env.FERN_CONFIG_BRANCH ?? env.GITHUB_HEAD_REF ?? env.GITHUB_REF_NAME ?? "", | ||
| config_pr_number: | ||
| env.FERN_CONFIG_PR_NUMBER ?? extractPrNumberFromGithubRef(env.GITHUB_REF) ?? null, |
There was a problem hiding this comment.
should we fallback to undefined instead?
| config_branch: env.FERN_CONFIG_BRANCH ?? env.GITHUB_HEAD_REF ?? env.GITHUB_REF_NAME ?? "", | ||
| config_pr_number: | ||
| env.FERN_CONFIG_PR_NUMBER ?? extractPrNumberFromGithubRef(env.GITHUB_REF) ?? null, | ||
| trigger: env.GITHUB_EVENT_NAME ?? "", |
There was a problem hiding this comment.
should we fallback to undefined instead?
| if (client !== null) { | ||
| return client; | ||
| } | ||
| if (!isGithubActionsRunner() || POSTHOG_API_KEY.length === 0) { |
There was a problem hiding this comment.
POSTHOG_API_KEY.length === 0 can be shortened to POSTHOG_API_KEY. It'll be falsy if null, undefined, or an empty string.
| */ | ||
| export function capturePostHogEvent(event: TelemetryEvent, context: AutomationContext): void { | ||
| const c = getClient(); | ||
| if (c === null) { |
There was a problem hiding this comment.
can be shortened to if (c) { return; }
| config_repo: string; | ||
| config_commit_sha: string; | ||
| config_branch: string; | ||
| config_pr_number: string | null; |
There was a problem hiding this comment.
would make this config_pr_number?: string or config_pr_number: string | undefined
| config_branch: string; | ||
| config_pr_number: string | null; | ||
| trigger: string; | ||
| cli_version: string | null; |
There was a problem hiding this comment.
cli_version?: or cli_version: string | undefined
| export function injectFernToken(token: string): void { | ||
| fernToken = token.length > 0 ? token : null; | ||
| } |
There was a problem hiding this comment.
this feel hacky, is there a way around this?
There was a problem hiding this comment.
Not really if we want to have also the parsing of the inputs instrumented
85cf925 to
1cfd61e
Compare
Wires every JS action through `instrumentAction` + `runPostCleanup` so each run emits structured `automation_run_started` / `automation_run_completed` / `wrapper_failed` events to four sinks: a `::fern-telemetry::<json>` log line, PostHog (always), Sentry (`wrapper_failed` only), and the Lightweight API (`/v1/automation/events`, `wrapper_failed` only). - New `packages/shared/src/obs/` module — telemetry-client, posthog, sentry, lightweight-api, automation-context, errors, types. All free functions; no class instances exposed. - `WrapperError(errorCode, message, originalError?)` is the single way for wrapper code to attach a stable SCREAMING_SNAKE error code to a thrown exception. Errors that aren't `WrapperError` get the generic `UNKNOWN_ERROR` code at classification time. - `injectFernToken(token)` (called from inside `instrumentAction`'s body after parsing) configures the Lightweight API auth so input-parsing failures still get classified before the token is available. - `flushTelemetry()` (called from `runAction` before `process.exit`) awaits every in-flight Lightweight API request, then shuts down the PostHog and Sentry SDK queues. - All 8 actions wired (preview, generate, upgrade, verify, sync-openapi, setup-cli, resolve-cli, verify-token). CLI invocations in `generate` and `sync-openapi` catch + re-throw as `WrapperError` with action-specific codes (`CLI_AUTOMATIONS_GENERATE_FAILED`, `CLI_GHA_PULL_SPEC_FAILED`, `CLI_GHA_SYNC_SPECS_FAILED`). `setup-cli` wraps `installFernCli` and `buildCliFromSource` likewise (`CLI_INSTALL_*`). Hardcoded build constants (`POSTHOG_API_KEY`, `SENTRY_DSN_AUTOMATIONS`, `LIGHTWEIGHT_API_URL`) are empty strings today; telemetry is a runtime no-op until they're populated. Sentry release tagging and source-maps upload also pending — see PR description for the follow-ups. Refs FER-10253. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1cfd61e to
0d89477
Compare
Summary
Wraps every JS action through a single telemetry pipeline that emits structured
automation_run_started/automation_run_completed/wrapper_failedevents to four sinks: a::fern-telemetry::<json>log line (always), PostHog (always), Sentry (failures only), and the Lightweight API at/v1/automation/events(failures only). Single touchpoint for actions:instrumentAction(name, fn)plusinjectFernToken(token)to authenticate the Lightweight API POST.Linear: FER-10253
Highlights
packages/shared/src/obs/module —telemetry-client,posthog,sentry,lightweight-api,automation-context,errors,types. All free functions, no class instances exposed.WrapperError(errorCode, message, originalError?)is the single way for wrapper code to attach a stable SCREAMING_SNAKE error code to a thrown exception. Anything that isn't aWrapperErrorgets classified asUNKNOWN_ERROR.injectFernToken(token)is called from insideinstrumentAction's body after parsing — so input-parsing failures still get classified aswrapper_failedvia the catch path before the token is available.flushTelemetry()runs fromrunAction's exit path: awaits in-flight Lightweight API POSTs (Promise.allSettled), then shuts down the PostHog and Sentry SDK queues.generate/sync-openapicatch + re-throw asWrapperErrorwith action-specific codes (CLI_AUTOMATIONS_GENERATE_FAILED,CLI_GHA_PULL_SPEC_FAILED,CLI_GHA_SYNC_SPECS_FAILED).setup-cliwrapsinstallFernCliand the source-build path inWrapperError("CLI_INSTALL_*", ...).outcome=cancelledin saved state; the post phase emitsautomation_run_completedwithstatus: cancelled. Nowrapper_failedfrom cancellation — it's not a wrapper-side fault.Follow-ups before this is "live"
Two things need to happen on top of this PR before any telemetry actually fires:
Wire the actual build constants.
POSTHOG_API_KEY,SENTRY_DSN_AUTOMATIONS, andLIGHTWEIGHT_API_URLinpackages/shared/src/obs/build-constants.tsare empty strings. The PostHog and Sentry SDKs initialize as no-ops when their constant is empty; the Lightweight API short-circuits when its URL is empty. Once theautomationsSentry project and the/v1/automation/eventsendpoint exist, hardcode the values here. (PostHog API keys and Sentry DSNs are designed to be embedded in client code — they're write-only at the project level. No CI secret needed.)Sentry release tagging + source-maps upload.
Sentry.init({ release: ... })is currently unset inpackages/shared/src/obs/sentry.ts, and source maps aren't uploaded — so any captured exception today would point at the bundleddist/index.jsline numbers, not the original TypeScript. Both depend on a CI-driven release pipeline that bakes a release tag at the moment dist is built. Open question: do we move release builds to CI (therelease.ymlheader already has a TODO about this) so we can runsentry-cli releases new <tag>+sentry-cli releases files upload-sourcemapskeyed to the same tag we pass toSentry.init? Or do we keep dist commits as-is and run a separate sourcemaps-upload workflow on tag publish?Either way the value passed to
Sentry.init({ release })and the Sentry CLI's release identifier need to agree, otherwise deobfuscation won't resolve. Worth a separate Linear ticket.Test plan
pnpm typecheck && pnpm check && pnpm test && pnpm build— local clean (typecheck across 10 packages, lint clean across 82 files, 51/51 shared tests pass, all 8 dists rebuilt at ~3.7 MB)setup-cli@v0.0.0-obs1) and trigger a workflow that uses it. Verify::fern-telemetry::log lines appear with the righteventnames and that PostHog / Sentry stay silent (constants still empty). Once constants are wired (follow-up 1), repeat and confirm events show up in PostHog / Sentry.generateorsync-openapi, verifywrapper_failedlog line carries the action-specificerror_code(e.g.CLI_AUTOMATIONS_GENERATE_FAILED).