Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,9 @@ transport.
"observability": {
"enabled": true, // master switch — without it CF captures NOTHING
"logs": { "enabled": true, "invocation_logs": true,
"head_sampling_rate": 1, "persist": true },
"head_sampling_rate": 1, "persist": true },
"traces": { "enabled": true,
"head_sampling_rate": 0.1, "persist": true }
"head_sampling_rate": 0.01, "persist": true }
},
"analytics_engine_datasets": [
{ "binding": "DECO_METRICS", "dataset": "deco_metrics_<site>" }
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@ The full v2 docs live at **[docs.deco.cx/v2](https://docs.deco.cx/v2/en/getting-
- [Migration](https://docs.deco.cx/v2/en/migration/overview) — v1 → v2 playbook + script + skill.
- [Case studies](https://docs.deco.cx/v2/en/case-studies/overview) — three production stores end-to-end.

In-repo references:

- [Observability](./docs/observability.md) — `instrumentWorker`, span/metric reference, Cloudflare wiring, ClickHouse query patterns.

---

## Peer dependencies
Expand Down
324 changes: 324 additions & 0 deletions docs/observability.md

Large diffs are not rendered by default.

22 changes: 21 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"deco-migrate": "./dist/scripts/migrate.cjs",
"deco-post-cleanup": "./dist/scripts/migrate-post-cleanup.cjs",
"deco-htmx-analyze": "./dist/scripts/htmx-analyze.cjs",
"deco-cf-observability": "./dist/scripts/migrate-to-cf-observability.cjs"
"deco-cf-observability": "./dist/scripts/migrate-to-cf-observability.cjs",
"deco-audit-observability": "./dist/scripts/audit-observability-config.cjs"
},
"exports": {
".": {
Expand Down Expand Up @@ -194,6 +195,21 @@
"import": "./dist/core/sdk/otelAdapters/clickhouseCollector.js",
"require": "./dist/core/sdk/otelAdapters/clickhouseCollector.cjs"
},
"./sdk/otelHttpMeter": {
"types": "./dist/core/sdk/otelHttpMeter.d.ts",
"import": "./dist/core/sdk/otelHttpMeter.js",
"require": "./dist/core/sdk/otelHttpMeter.cjs"
},
"./sdk/otelHttpErrorLog": {
"types": "./dist/core/sdk/otelHttpErrorLog.d.ts",
"import": "./dist/core/sdk/otelHttpErrorLog.js",
"require": "./dist/core/sdk/otelHttpErrorLog.cjs"
},
"./sdk/urlRedaction": {
"types": "./dist/core/sdk/urlRedaction.d.ts",
"import": "./dist/core/sdk/urlRedaction.js",
"require": "./dist/core/sdk/urlRedaction.cjs"
},
"./sdk/observability": {
"types": "./dist/tanstack/sdk/observability.d.ts",
"import": "./dist/tanstack/sdk/observability.js",
Expand Down Expand Up @@ -356,6 +372,10 @@
"types": "./dist/scripts/migrate-to-cf-observability.d.ts",
"default": "./dist/scripts/migrate-to-cf-observability.cjs"
},
"./scripts/audit-observability-config": {
"types": "./dist/scripts/audit-observability-config.d.ts",
"default": "./dist/scripts/audit-observability-config.cjs"
},
"./scripts/tailwind-lint": {
"types": "./dist/scripts/tailwind-lint.d.ts",
"default": "./dist/scripts/tailwind-lint.cjs"
Expand Down
196 changes: 196 additions & 0 deletions scripts/audit-observability-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { auditObservabilityBlock } from "./audit-observability-config";
import { parseJsonc, stripJsoncTrailingCommas } from "./lib/jsonc";

describe("auditObservabilityBlock", () => {
it("flags missing observability block as error", () => {
const findings = auditObservabilityBlock(undefined);
expect(findings).toHaveLength(1);
expect(findings[0].id).toBe("observability_missing");
expect(findings[0].severity).toBe("error");
expect(findings[0].fix).toContain("deco-cf-observability --write");
});

it("returns no findings for canonical block (traces 0.01, logs 1, persist)", () => {
const findings = auditObservabilityBlock({
enabled: true,
logs: { enabled: true, head_sampling_rate: 1, persist: true },
traces: { enabled: true, head_sampling_rate: 0.01, persist: true },
});
expect(findings).toEqual([]);
});

it("flags head_sampling_rate_elevated as error when traces rate > 0.01", () => {
const findings = auditObservabilityBlock({
enabled: true,
logs: { enabled: true, head_sampling_rate: 1, persist: true },
traces: { enabled: true, head_sampling_rate: 0.1, persist: true },
});
const elevated = findings.find((f) => f.id === "head_sampling_rate_elevated");
expect(elevated).toBeDefined();
expect(elevated?.severity).toBe("error");
expect(elevated?.message).toContain("0.1");
expect(elevated?.fix).toContain("--traces-rate 0.01");
});

it("does not flag boundary value 0.01", () => {
const findings = auditObservabilityBlock({
enabled: true,
logs: { enabled: true, head_sampling_rate: 1, persist: true },
traces: { enabled: true, head_sampling_rate: 0.01, persist: true },
});
expect(findings.find((f) => f.id === "head_sampling_rate_elevated")).toBeUndefined();
});

it("flags observability_disabled when enabled: false at the top", () => {
const findings = auditObservabilityBlock({
enabled: false,
logs: { enabled: true, head_sampling_rate: 1, persist: true },
traces: { enabled: true, head_sampling_rate: 0.01, persist: true },
});
const f = findings.find((x) => x.id === "observability_disabled");
expect(f?.severity).toBe("error");
});

it("flags traces_disabled and logs_disabled separately", () => {
const findings = auditObservabilityBlock({
enabled: true,
logs: { enabled: false, head_sampling_rate: 1, persist: true },
traces: { enabled: false, head_sampling_rate: 0.01, persist: true },
});
expect(findings.some((f) => f.id === "traces_disabled" && f.severity === "warn")).toBe(true);
expect(findings.some((f) => f.id === "logs_disabled" && f.severity === "warn")).toBe(true);
});

it("flags logs_head_sampling_rate_low when logs rate < 1", () => {
const findings = auditObservabilityBlock({
enabled: true,
logs: { enabled: true, head_sampling_rate: 0.5, persist: true },
traces: { enabled: true, head_sampling_rate: 0.01, persist: true },
});
const f = findings.find((x) => x.id === "logs_head_sampling_rate_low");
expect(f?.severity).toBe("warn");
});

it("flags persist_disabled_no_destination on traces", () => {
const findings = auditObservabilityBlock({
enabled: true,
logs: { enabled: true, head_sampling_rate: 1, persist: true },
traces: { enabled: true, head_sampling_rate: 0.01, persist: false },
});
const f = findings.find((x) => x.id === "persist_disabled_no_destination");
expect(f?.severity).toBe("error");
});

it("accepts persist:false when a destination is configured", () => {
const findings = auditObservabilityBlock({
enabled: true,
logs: {
enabled: true,
head_sampling_rate: 1,
persist: false,
destinations: [{ id: "my-logs-dest" }],
},
traces: {
enabled: true,
head_sampling_rate: 0.01,
persist: false,
destinations: [{ id: "my-traces-dest" }],
},
});
expect(findings).toEqual([]);
});

it("does not flag persist on a disabled traces block", () => {
const findings = auditObservabilityBlock({
enabled: true,
logs: { enabled: true, head_sampling_rate: 1, persist: true },
traces: { enabled: false, head_sampling_rate: 0.01, persist: false },
});
// traces_disabled fires, but not persist_disabled_no_destination for traces
expect(findings.find((f) => f.id === "persist_disabled_no_destination")).toBeUndefined();
});

it("treats absent head_sampling_rate as not-elevated (no false positive)", () => {
const findings = auditObservabilityBlock({
enabled: true,
logs: { enabled: true, persist: true },
traces: { enabled: true, persist: true },
});
expect(findings.find((f) => f.id === "head_sampling_rate_elevated")).toBeUndefined();
expect(findings.find((f) => f.id === "logs_head_sampling_rate_low")).toBeUndefined();
});

it("stacks multiple findings on a deeply-broken config", () => {
const findings = auditObservabilityBlock({
enabled: true,
logs: { enabled: true, head_sampling_rate: 0.1, persist: false },
traces: { enabled: true, head_sampling_rate: 0.5, persist: false },
});
const ids = findings.map((f) => f.id).sort();
expect(ids).toContain("head_sampling_rate_elevated");
expect(ids).toContain("logs_head_sampling_rate_low");
// both traces and logs each emit a persist_disabled_no_destination
expect(ids.filter((i) => i === "persist_disabled_no_destination")).toHaveLength(2);
});
});

describe("JSONC handling — trailing commas + comments", () => {
it("stripJsoncTrailingCommas removes commas before `}` and `]`", () => {
expect(stripJsoncTrailingCommas(`{ "a": 1, "b": 2, }`)).toBe(`{ "a": 1, "b": 2 }`);
expect(stripJsoncTrailingCommas(`{ "a": [1, 2, 3,], }`)).toBe(`{ "a": [1, 2, 3] }`);
});

it("stripJsoncTrailingCommas preserves commas INSIDE string literals", () => {
expect(stripJsoncTrailingCommas(`{ "a": "hello,], world", }`)).toBe(
`{ "a": "hello,], world" }`,
);
});

it("parseJsonc accepts both line comments and trailing commas", () => {
const src = `{
// a wrangler.jsonc-style config
"observability": {
"enabled": true,
"traces": { "enabled": true, "head_sampling_rate": 0.01, "persist": true, },
"logs": { "enabled": true, "head_sampling_rate": 1, "persist": true, },
},
}`;
expect(parseJsonc<{ observability: { enabled: boolean } }>(src).observability.enabled).toBe(
true,
);
});
});

describe("CLI smoke — wrangler.jsonc with trailing commas", () => {
let tmpdir: string;
beforeEach(() => {
tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "audit-jsonc-"));
});
afterEach(() => {
fs.rmSync(tmpdir, { recursive: true, force: true });
});

it("audits a canonical wrangler.jsonc containing trailing commas without parse failure", () => {
const src = `{
// canonical, with trailing commas — common in real wrangler.jsonc files
"name": "my-store",
"observability": {
"enabled": true,
"traces": { "enabled": true, "head_sampling_rate": 0.01, "persist": true, },
"logs": { "enabled": true, "head_sampling_rate": 1, "persist": true, },
},
}`;
fs.writeFileSync(path.join(tmpdir, "wrangler.jsonc"), src);
// The audit's pure function still works against the parsed shape; this
// test guards the parse step itself, which previously threw on the
// trailing commas.
const parsed = parseJsonc<{
observability?: Parameters<typeof auditObservabilityBlock>[0];
}>(src);
expect(auditObservabilityBlock(parsed.observability)).toEqual([]);
});
});
Loading
Loading