mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
738b200e5a
* feat(cli): send traces, metrics, and the wide event to Axiom Moves the CLI's telemetry onto the Axiom transport added in the previous commit. Sentry keeps crash reporting — its source-map symbolication, issue grouping, and quotable event id have no Axiom equivalent — so error reports, the prefilled issue link, and the JSON report's `sentryEventId` are unchanged. Because Effect exposes a single `Tracer` reference, the two backends are mutually exclusive for spans, so `tracesSampleRate` is pinned to 0 and the Effect-to-Sentry bridge is deleted. Metrics and spans move together because they share one runtime, and that sharing is load-bearing. `telemetry-runtime.ts` builds the layer once into a long-lived scope; the run root span, the scan program, and the renderer all draw their tracer from it. Child spans then nest under the run span natively, with no `ExternalSpan` stitching. More importantly, Effect tracks delta-temporality state per metrics exporter instance, so building the layer per program would make every new exporter re-report each counter in full — silently doubling every metric. `shutdownTelemetry()` is memoized for the same reason: several exit paths can fire, and a `--debug` user error flushes before the top-level catch does. `recordCount` / `recordDistribution` keep their exact signatures, so all ~40 call sites are untouched; only the body changes to write into Effect's metric registry. Two constraints differ from Sentry: Effect metric attributes are string-only, so numbers and booleans are stringified while null and undefined are dropped rather than coerced to "null"; and histograms need explicit buckets, so distributions share `METRIC_DISTRIBUTION_BOUNDARIES`. Metrics never touch the tracer, so they are scrubbed at the emit site instead of by the scrubbing tracer. `--debug` now prints the Axiom trace id, and crashes carry that id plus `runId` as Sentry tags — so one pasted id resolves in both backends, replacing the propagation-context linkage that dies with Sentry tracing. Verified end to end against a local collector with a real scan: one trace request and one metrics request, root span with `runInspect` nested under it, the full wide event, `scan.completed` recorded exactly once, and no home directory or username on the wire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(cli): close six review findings in the Axiom cutover Restore export to a user's own collector. `applyObservability` installed a backend only when first-party Axiom credentials resolved, so anyone who had pointed React Doctor at their own OTLP endpoint silently stopped getting spans once the CLI stopped falling back to `layerUserOtlp` unconditionally. The shared runtime now builds whenever either first-party credentials or `REACT_DOCTOR_OTLP_*` are present, and `layerObservability` still prefers the user's endpoint. Export metrics for commands that never scan. `version`, `install`, `ci`, `rules`, and early `inspect` exits record counters like `cli.invoked`, but only the scan path built the exporter — so `shutdownTelemetry` found no scope and dropped them. It now ensures the runtime exists before closing it, which is a no-op when a scan already opened one. Namespace the environment overrides as `REACT_DOCTOR_AXIOM_*`. The bare `AXIOM_TOKEN` / `AXIOM_DATASET` names are Axiom's own, and anyone already running Axiom likely has them exported — reading them would redirect React Doctor's telemetry into an unrelated account without the user asking. Carry the Axiom trace id and run id on crash reports as a *context* rather than tags. Both are unique per run, and tags are the indexed low-cardinality dimension — AGENTS.md calls this out for `runId` specifically. Also replaces an inline nanosecond conversion with a named constant, and defers `LSP_TELEMETRY_EXPORT_INTERVAL_MS` to the commit that actually reads it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(cli): embed the Axiom ingest token Enables first-party telemetry. Until now the constant was empty, so `resolveAxiomTelemetryOptions` returned null and the CLI recorded into Effect's registry and dropped everything on exit — the merge blocker called out on this stack. The token is `ingest:create` only, scoped to the `react-doctor` (Events) and `react-doctor-metrics` (OTel Metrics) datasets, with no expiry and no organization permissions. It ships in the published tarball and is therefore extractable; that was the deliberate trade (the alternative — proxying ingest through `www.react.doctor`, which already hosts the score API — remains available if abuse shows up). Detection is an Axiom monitor on anomalous ingest volume; rotation is a patch release. Verified against the live service: `/v1/traces` and `/v1/metrics` both return 200 for protobuf payloads with their respective dataset headers, which also confirms the protobuf serialization choice end to end rather than just against a local collector. Two tests moved with it. They asserted the "no token configured" path, which is no longer reachable through the environment; they now assert the embedded defaults resolve, and — the part that actually matters — that Axiom's own unprefixed `AXIOM_*` variables still cannot redirect the destination. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
84 lines
3.0 KiB
TypeScript
84 lines
3.0 KiB
TypeScript
import { AXIOM_DEFAULT_DOMAIN } from "@react-doctor/core";
|
|
import * as Redacted from "effect/Redacted";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test";
|
|
import { AXIOM_METRICS_DATASET, AXIOM_TRACES_DATASET } from "../src/cli/utils/constants.js";
|
|
import { resolveAxiomTelemetryOptions } from "../src/cli/utils/resolve-axiom-telemetry-options.js";
|
|
|
|
const TELEMETRY_ENVIRONMENT_VARIABLES = [
|
|
"VITEST",
|
|
"NODE_ENV",
|
|
"REACT_DOCTOR_NO_TELEMETRY",
|
|
"REACT_DOCTOR_AXIOM_TOKEN",
|
|
"REACT_DOCTOR_AXIOM_DOMAIN",
|
|
"REACT_DOCTOR_AXIOM_DATASET",
|
|
"REACT_DOCTOR_AXIOM_METRICS_DATASET",
|
|
"AXIOM_TOKEN",
|
|
"AXIOM_DOMAIN",
|
|
"AXIOM_DATASET",
|
|
] as const;
|
|
|
|
let saved: Record<string, string | undefined>;
|
|
|
|
beforeEach(() => {
|
|
saved = {};
|
|
for (const name of TELEMETRY_ENVIRONMENT_VARIABLES) {
|
|
saved[name] = process.env[name];
|
|
delete process.env[name];
|
|
}
|
|
});
|
|
|
|
afterEach(() => {
|
|
for (const name of TELEMETRY_ENVIRONMENT_VARIABLES) {
|
|
const previous = saved[name];
|
|
if (previous === undefined) delete process.env[name];
|
|
else process.env[name] = previous;
|
|
}
|
|
});
|
|
|
|
describe("resolveAxiomTelemetryOptions", () => {
|
|
it("resolves the embedded first-party defaults when nothing is overridden", () => {
|
|
const options = resolveAxiomTelemetryOptions();
|
|
|
|
expect(options).not.toBeNull();
|
|
expect(options?.domain).toBe(AXIOM_DEFAULT_DOMAIN);
|
|
expect(options?.tracesDataset).toBe(AXIOM_TRACES_DATASET);
|
|
expect(options?.metricsDataset).toBe(AXIOM_METRICS_DATASET);
|
|
});
|
|
|
|
it("ignores Axiom's own unprefixed environment variables", () => {
|
|
// A developer who already uses Axiom very likely has these set for their
|
|
// own app. Reading them would silently redirect React Doctor's telemetry
|
|
// into an unrelated account, or fail against a dataset that isn't shaped
|
|
// for it.
|
|
process.env.AXIOM_TOKEN = "someone-elses-token";
|
|
process.env.AXIOM_DOMAIN = "https://someone-elses-host.invalid";
|
|
process.env.AXIOM_DATASET = "someone-elses-dataset";
|
|
|
|
const options = resolveAxiomTelemetryOptions();
|
|
|
|
expect(options?.domain).toBe(AXIOM_DEFAULT_DOMAIN);
|
|
expect(options?.tracesDataset).toBe(AXIOM_TRACES_DATASET);
|
|
expect(Redacted.value(options?.token ?? Redacted.make(""))).not.toBe("someone-elses-token");
|
|
});
|
|
|
|
it("honors the REACT_DOCTOR_-prefixed overrides", () => {
|
|
process.env.REACT_DOCTOR_AXIOM_TOKEN = "our-token";
|
|
process.env.REACT_DOCTOR_AXIOM_DOMAIN = "https://example.invalid";
|
|
process.env.REACT_DOCTOR_AXIOM_DATASET = "our-traces";
|
|
process.env.REACT_DOCTOR_AXIOM_METRICS_DATASET = "our-metrics";
|
|
|
|
const options = resolveAxiomTelemetryOptions();
|
|
|
|
expect(options?.domain).toBe("https://example.invalid");
|
|
expect(options?.tracesDataset).toBe("our-traces");
|
|
expect(options?.metricsDataset).toBe("our-metrics");
|
|
});
|
|
|
|
it("stays null when telemetry is opted out even with a token present", () => {
|
|
process.env.REACT_DOCTOR_AXIOM_TOKEN = "our-token";
|
|
process.env.REACT_DOCTOR_NO_TELEMETRY = "1";
|
|
|
|
expect(resolveAxiomTelemetryOptions()).toBeNull();
|
|
});
|
|
});
|