@geonosis/observability 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,204 @@
1
+ import * as better_result from 'better-result';
2
+ import { Result } from 'better-result';
3
+ import { A as Attributes, E as ErrorSink, L as LastEventRecord, T as Tracer, S as SpanRecord } from './ports-Ct5eq-HS.js';
4
+ export { a as AttributeValue, C as CaptureReceipt, b as SinkFailure } from './ports-Ct5eq-HS.js';
5
+
6
+ /**
7
+ * The vocabulary, enforced.
8
+ *
9
+ * Two spellings of the same rules, on purpose. `parseAttributes` is structural and has no
10
+ * dependencies, so the sinks in this package validate without importing anything — and
11
+ * `attributesSchema(z)` builds the same rules over the CALLER's zod, because both consumers run
12
+ * different copies of it (4.2.0, 4.4.3 and 4.5.4 measured across the two trees in
13
+ * `docs/ports-events-inventory-2026-08-30.md`) and neither `@geonosis/events` nor
14
+ * `@geonosis/integrations` will import one. A test drives every case through both and asserts they
15
+ * agree; two validators that could disagree are worse than one.
16
+ */
17
+ declare const ATTRIBUTE_KEYS: readonly ["actor", "envelopeId", "release", "runtime", "service", "tenant"];
18
+ type AttributeKey = (typeof ATTRIBUTE_KEYS)[number];
19
+ declare const SPAN_NAME_CONVENTION = "A span name is `<area>.<action>`, lowercase, at least two dot-separated segments, letters and digits with dashes inside a segment: `queue.handle-batch`, `db.cell.query`. A name is a dimension in every trace query, so `queue.handle-batch` and `Queue Handle Batch` in one tree are two dimensions where the author meant one.";
20
+ declare const InvalidAttributes_base: better_result.TaggedErrorClass<"InvalidAttributes">;
21
+ declare class InvalidAttributes extends InvalidAttributes_base<{
22
+ message: string;
23
+ }> {
24
+ }
25
+ declare const parseAttributes: (value: unknown) => Result<Attributes, InvalidAttributes>;
26
+ /** What a caller gets back: enough of a schema to embed, and nothing this package had to import. */
27
+ type ZodSchema = {
28
+ safeParse: (value: unknown) => {
29
+ data?: unknown;
30
+ success: boolean;
31
+ };
32
+ };
33
+ /**
34
+ * The same vocabulary as a schema, built over the caller's zod, for a consumer whose contract layer
35
+ * already speaks schemas. The shape is declared so the schema is introspectable; every rule that is
36
+ * not a shape — a blank service, a tenant that is a person, an `extra` that shadows the vocabulary —
37
+ * comes from `problemsOf`, the one function `parseAttributes` also uses. Two validators that could
38
+ * disagree would be worse than one, so there is only one set of rules and two doors to it.
39
+ */
40
+ declare const attributesSchema: (z: unknown) => ZodSchema;
41
+
42
+ /**
43
+ * At-least-once in, at-most-once counted.
44
+ *
45
+ * Cloudflare Queues redelivers, and during.day recognises a redelivery with
46
+ * `handleQueue({ seen: (id) => hasAuditEntryForEvent(db, { eventId: id }) })` — a database read.
47
+ * The store is the consumer's (a Map in one isolate, an audit table in a cell, a KV namespace at an
48
+ * edge), so only the COUNTING is portable and the store arrives as two functions.
49
+ *
50
+ * The rule that matters is the one about failure: an envelope whose capture FAILED is not
51
+ * remembered. Remembering it before the sink answered would buy at-most-once by losing
52
+ * at-least-once — one transport error and that envelope is silent for ever.
53
+ */
54
+ type DedupeStore = {
55
+ /** Record that this envelope was filed, and under which id. */
56
+ remember: (envelopeId: string, id: string) => Promise<void> | void;
57
+ /** The id this envelope was filed under before, or undefined if it has not been. */
58
+ seen: (envelopeId: string) => Promise<string | undefined> | string | undefined;
59
+ };
60
+ declare const withDedupe: (sink: ErrorSink, store?: DedupeStore) => ErrorSink;
61
+
62
+ /**
63
+ * The producer half of the file contract `geonosis-doctor`'s `observability` check reads.
64
+ *
65
+ * The doctor imports nothing of this package — it parses `{ at, id, sink }` off disk — because a
66
+ * check that needed the library it checks cannot run in the tree where the library is missing,
67
+ * which is the first case it exists to find. So the shape lives here and the reader lives there,
68
+ * and both are tested against the same field names.
69
+ *
70
+ * The WRITER is injected. A file is a Node fact; on workerd the same record goes to a KV namespace
71
+ * or a D1 row, and a package that reached for `node:fs` would not load in the runtime that has most
72
+ * of the errors.
73
+ */
74
+ declare const withLastEvent: (sink: ErrorSink, record: (event: LastEventRecord) => Promise<void>, now?: () => number) => ErrorSink;
75
+
76
+ /**
77
+ * The sink tests, the probe and a no-key deployment all read.
78
+ *
79
+ * dielime's LLM seam is the posture: its no-key path is a real deterministic implementation that
80
+ * runs the whole loop, not a null object. during.day's `silentAnalyticsSink` is the other posture,
81
+ * and its own comment has to argue it is "not a null object hiding a mistake". A sink you can read
82
+ * needs no such argument.
83
+ */
84
+ type CapturedError = {
85
+ at: number;
86
+ attributes: Attributes;
87
+ error: Error;
88
+ id: string;
89
+ };
90
+ type MemorySink = ErrorSink & {
91
+ readonly captured: readonly CapturedError[];
92
+ clear: () => void;
93
+ readonly flushes: number;
94
+ };
95
+ /** The envelope id when there is one, so a redelivery lands on the row its first delivery made. */
96
+ declare const idFor: (attributes: Attributes) => string;
97
+ declare const createMemorySink: (options?: {
98
+ now?: () => number;
99
+ }) => MemorySink;
100
+ /**
101
+ * during.day's `silentAnalyticsSink` with the `console.info` taken out: it answers ok and keeps
102
+ * nothing. Shipped ON PURPOSE, because it is the production path in both consumers whenever no key
103
+ * is configured — and because `--prove` must be able to catch it. A probe that could not tell this
104
+ * apart from a real sink would be the fifth lying instrument.
105
+ */
106
+ declare const createSwallowingSink: () => ErrorSink;
107
+
108
+ /**
109
+ * D-029 for a sink: every gate ships its own falsification probe.
110
+ *
111
+ * The ratchet compares a number against a number and cannot tell a counter that found nothing from
112
+ * a counter that CAN find nothing. A sink is worse — it answers `ok` either way, and during.day's
113
+ * `silentAnalyticsSink` answers `ok` in production whenever no key is set. So the probe plants an
114
+ * error under an id nothing else in the world has, and then goes and LOOKS.
115
+ */
116
+ type ObservabilityVerdict = 'cannot-fail' | 'cannot-measure' | 'misread' | 'proven';
117
+ type SinkProbe = {
118
+ /** Whatever the probe opened — a server, a client, a connection. Closed whatever the verdict. */
119
+ close?: () => Promise<void>;
120
+ /** The sink, in a verdict. */
121
+ kind: string;
122
+ /**
123
+ * Every id this sink can see right now. EMPTY is the answer that separates a swallower from a
124
+ * misfiler: a sink holding nothing at all cannot fail, and one holding something that is not the
125
+ * plant filed it under an id nobody was given.
126
+ */
127
+ seen: (traceId: string) => Promise<readonly string[]>;
128
+ sink: ErrorSink;
129
+ };
130
+ type ObservabilityProof = {
131
+ /** What the probe could see when it gave up. Named in a MISREAD, because it is the evidence. */
132
+ found?: readonly string[];
133
+ kind: string;
134
+ reason?: string;
135
+ traceId?: string;
136
+ verdict: ObservabilityVerdict;
137
+ };
138
+ declare const PROBE_SERVICE = "geonosis-observability-probe";
139
+ /** The memory sink and a way to read it — what the kit's own gate and `--sink memory` prove over. */
140
+ declare const memoryProbe: () => SinkProbe & {
141
+ sink: MemorySink;
142
+ };
143
+ declare const proveObservability: ({ attributes, probe, timeoutMs, }: {
144
+ /** What the plant says about itself. Defaults to this probe's own service name. */
145
+ attributes?: Partial<Attributes>;
146
+ /** Absent means nothing is configured, which is a refusal and never a pass. */
147
+ probe?: SinkProbe;
148
+ timeoutMs?: number;
149
+ }) => Promise<ObservabilityProof>;
150
+ declare const formatProof: (proof: ObservabilityProof) => string;
151
+ /** PROVEN is the only pass. Two, never one: a refusal is not the same as a finding. */
152
+ declare const exitCodeOf: (proof: ObservabilityProof) => number;
153
+
154
+ declare const isSpanName: (name: string) => boolean;
155
+
156
+ type MemoryTracer = Tracer & {
157
+ clear: () => void;
158
+ readonly spans: readonly SpanRecord[];
159
+ };
160
+ /**
161
+ * A tracer records; it does not decide. The body's value comes back untouched and the body's throw
162
+ * goes back up untouched — a tracer that swallowed one would make a failed request answer 200,
163
+ * which is the third lying instrument of #42 rebuilt on purpose.
164
+ *
165
+ * Attributes that break the vocabulary are recorded as given rather than refused, for the same
166
+ * reason a span name is: instrumentation must not be the thing that takes a request down. The sink
167
+ * refuses; the tracer reports.
168
+ */
169
+ declare const createMemoryTracer: (options?: {
170
+ now?: () => number;
171
+ }) => MemoryTracer;
172
+ /** Whether a tracer's attributes would have been accepted by a sink. For tests and for a probe. */
173
+ declare const spanAttributesAreValid: (span: SpanRecord) => boolean;
174
+
175
+ /**
176
+ * A dedupe key that survives the vendor.
177
+ *
178
+ * Measured against `posthog-node@5.50.0` over a stub server: a `uuid` that is not a valid UUID is
179
+ * REPLACED with a generated one, silently. during.day's envelope ids are `crypto.randomUUID()` so
180
+ * its dedupe works; a consumer whose ids are prefixed (`evt_…`, a ULID, a nanoid, a Medusa
181
+ * `order_01H…`) would hand over a dedupe key that never arrives and read every redelivery as a new
182
+ * incident, for ever, with nothing anywhere saying so.
183
+ *
184
+ * So an id that is already a UUID goes through untouched — during.day's shape, unchanged — and one
185
+ * that is not becomes a UUIDv5 derived from it: same envelope, same uuid, every time, in every
186
+ * process. The original goes on the wire as a property, because a stable uuid nobody can guess is
187
+ * not something a human greps for.
188
+ */
189
+ declare const isUuid: (value: string) => boolean;
190
+ /**
191
+ * UUIDv5 of `value`, under this package's namespace unless another is named. Deterministic across
192
+ * processes and runtimes — the namespace is a parameter so the published RFC 4122 §4.3 vectors can
193
+ * be run against it: self-consistency would be satisfied by any hash at all, and two services
194
+ * deriving an id for the same envelope have to agree even when one of them is not running this code.
195
+ */
196
+ declare const uuidFrom: (value: string, inNamespace?: string) => Promise<string>;
197
+ declare const randomUuid: () => string;
198
+ /**
199
+ * The uuid a vendor is given for this envelope: the envelope id when it already is one, a stable
200
+ * derivation when it is not, a fresh one when there is no envelope at all.
201
+ */
202
+ declare const uuidForEnvelope: (envelopeId: string | undefined) => Promise<string>;
203
+
204
+ export { ATTRIBUTE_KEYS, type AttributeKey, Attributes, type CapturedError, type DedupeStore, ErrorSink, InvalidAttributes, LastEventRecord, type MemorySink, type MemoryTracer, type ObservabilityProof, type ObservabilityVerdict, PROBE_SERVICE, SPAN_NAME_CONVENTION, type SinkProbe, SpanRecord, Tracer, type ZodSchema, attributesSchema, createMemorySink, createMemoryTracer, createSwallowingSink, exitCodeOf, formatProof, idFor, isSpanName, isUuid, memoryProbe, parseAttributes, proveObservability, randomUuid, spanAttributesAreValid, uuidForEnvelope, uuidFrom, withDedupe, withLastEvent };
package/dist/index.js ADDED
@@ -0,0 +1,145 @@
1
+ import {
2
+ PROBE_SERVICE,
3
+ createMemorySink,
4
+ createSwallowingSink,
5
+ exitCodeOf,
6
+ formatProof,
7
+ idFor,
8
+ memoryProbe,
9
+ proveObservability
10
+ } from "./chunk-UFR2R2VM.js";
11
+ import {
12
+ ATTRIBUTE_KEYS,
13
+ InvalidAttributes,
14
+ SPAN_NAME_CONVENTION,
15
+ SinkFailure,
16
+ attributesSchema,
17
+ isUuid,
18
+ parseAttributes,
19
+ randomUuid,
20
+ uuidForEnvelope,
21
+ uuidFrom
22
+ } from "./chunk-2ZTQU3OO.js";
23
+
24
+ // src/dedupe.ts
25
+ import { Result } from "better-result";
26
+ var inMemoryStore = () => {
27
+ const rows = /* @__PURE__ */ new Map();
28
+ return {
29
+ remember: (envelopeId, id) => {
30
+ rows.set(envelopeId, id);
31
+ },
32
+ seen: (envelopeId) => rows.get(envelopeId)
33
+ };
34
+ };
35
+ var withDedupe = (sink, store = inMemoryStore()) => ({
36
+ capture: async (error, attributes) => {
37
+ const parsed = parseAttributes(attributes);
38
+ if (parsed.isErr()) {
39
+ return Result.err(
40
+ new SinkFailure({
41
+ cause: parsed.error,
42
+ message: parsed.error.message,
43
+ sink: `${sink.kind}+dedupe`
44
+ })
45
+ );
46
+ }
47
+ const taken = parsed.unwrap();
48
+ const envelopeId = taken.envelopeId;
49
+ if (envelopeId === void 0) return sink.capture(error, taken);
50
+ const before = await store.seen(envelopeId);
51
+ if (before !== void 0) return Result.ok({ deduplicated: true, id: before });
52
+ const receipt = await sink.capture(error, taken);
53
+ if (receipt.isErr()) return receipt;
54
+ await store.remember(envelopeId, receipt.unwrap().id);
55
+ return receipt;
56
+ },
57
+ flush: () => sink.flush(),
58
+ kind: `${sink.kind}+dedupe`
59
+ });
60
+
61
+ // src/last-event.ts
62
+ var withLastEvent = (sink, record, now = Date.now) => ({
63
+ capture: async (error, attributes) => {
64
+ const receipt = await sink.capture(error, attributes);
65
+ if (receipt.isErr() || receipt.value.deduplicated) return receipt;
66
+ try {
67
+ await record({
68
+ at: now(),
69
+ id: receipt.value.id,
70
+ ...attributes.release === void 0 ? {} : { release: attributes.release },
71
+ sink: sink.kind
72
+ });
73
+ } catch {
74
+ }
75
+ return receipt;
76
+ },
77
+ flush: () => sink.flush(),
78
+ kind: sink.kind
79
+ });
80
+
81
+ // src/span.ts
82
+ var SPAN_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)+$/;
83
+ var isSpanName = (name) => SPAN_NAME.test(name);
84
+
85
+ // src/tracer.ts
86
+ var createMemoryTracer = (options = {}) => {
87
+ const now = options.now ?? (() => performance.now());
88
+ const spans = [];
89
+ return {
90
+ clear: () => {
91
+ spans.length = 0;
92
+ },
93
+ kind: "memory",
94
+ span: async (name, attributes, body) => {
95
+ const started = now();
96
+ const record = (outcome, error) => {
97
+ spans.push({
98
+ attributes,
99
+ conventional: isSpanName(name),
100
+ durationMs: now() - started,
101
+ ...error === void 0 ? {} : { error },
102
+ name,
103
+ outcome
104
+ });
105
+ };
106
+ try {
107
+ const value = await body();
108
+ record("ok");
109
+ return value;
110
+ } catch (thrown) {
111
+ record("error", thrown instanceof Error ? thrown : new Error(String(thrown)));
112
+ throw thrown;
113
+ }
114
+ },
115
+ get spans() {
116
+ return spans;
117
+ }
118
+ };
119
+ };
120
+ var spanAttributesAreValid = (span) => parseAttributes(span.attributes).isOk();
121
+ export {
122
+ ATTRIBUTE_KEYS,
123
+ InvalidAttributes,
124
+ PROBE_SERVICE,
125
+ SPAN_NAME_CONVENTION,
126
+ SinkFailure,
127
+ attributesSchema,
128
+ createMemorySink,
129
+ createMemoryTracer,
130
+ createSwallowingSink,
131
+ exitCodeOf,
132
+ formatProof,
133
+ idFor,
134
+ isSpanName,
135
+ isUuid,
136
+ memoryProbe,
137
+ parseAttributes,
138
+ proveObservability,
139
+ randomUuid,
140
+ spanAttributesAreValid,
141
+ uuidForEnvelope,
142
+ uuidFrom,
143
+ withDedupe,
144
+ withLastEvent
145
+ };
@@ -0,0 +1,229 @@
1
+ import {
2
+ exitCodeOf,
3
+ formatProof,
4
+ memoryProbe,
5
+ proveObservability
6
+ } from "./chunk-UFR2R2VM.js";
7
+ import {
8
+ createPostHogSink
9
+ } from "./chunk-EJS3A3ZV.js";
10
+ import "./chunk-2ZTQU3OO.js";
11
+
12
+ // src/observability-cli.ts
13
+ import { spawnSync } from "child_process";
14
+
15
+ // src/posthog/stub.ts
16
+ import { createServer } from "http";
17
+ import { gunzipSync } from "zlib";
18
+ var bodyOf = (chunks, encoding) => {
19
+ const raw = Buffer.concat(chunks);
20
+ return encoding === "gzip" ? gunzipSync(raw).toString("utf8") : raw.toString("utf8");
21
+ };
22
+ var eventsIn = (text) => {
23
+ const parsed = JSON.parse(text);
24
+ return parsed.batch ?? [];
25
+ };
26
+ var listening = (server) => new Promise((resolve, reject) => {
27
+ server.once("error", reject);
28
+ server.listen(0, "127.0.0.1", () => {
29
+ const address = server.address();
30
+ if (address === null || typeof address === "string") {
31
+ reject(new Error("the stub server did not take a port"));
32
+ return;
33
+ }
34
+ resolve(address.port);
35
+ });
36
+ });
37
+ var startPostHogStub = async ({
38
+ status = 200
39
+ } = {}) => {
40
+ const events = [];
41
+ let requests = 0;
42
+ const server = createServer((request, response) => {
43
+ const chunks = [];
44
+ request.on("data", (chunk) => chunks.push(chunk));
45
+ request.on("end", () => {
46
+ requests += 1;
47
+ try {
48
+ events.push(...eventsIn(bodyOf(chunks, request.headers["content-encoding"])));
49
+ } catch {
50
+ }
51
+ response.writeHead(status, { "content-type": "application/json" });
52
+ response.end(status === 200 ? '{"status":"Ok"}' : '{"error":"refused"}');
53
+ });
54
+ });
55
+ const port = await listening(server);
56
+ return {
57
+ close: () => new Promise((resolve) => {
58
+ server.closeAllConnections();
59
+ server.close(() => resolve());
60
+ }),
61
+ get events() {
62
+ return events;
63
+ },
64
+ get requests() {
65
+ return requests;
66
+ },
67
+ url: `http://127.0.0.1:${port}`
68
+ };
69
+ };
70
+
71
+ // src/observability-cli.ts
72
+ var USAGE = `geonosis-observability prove --sink <memory|posthog-stub|posthog> [--timeout <seconds>] [--verify-command <cmd>]
73
+
74
+ Plants an error with a trace id nothing else in the world has, and then goes and LOOKS for it. A
75
+ sink answers ok whether or not it kept anything \u2014 during.day's no-key sink answers ok in production
76
+ \u2014 so a gate over one is green for ever unless something reads the sink back.
77
+
78
+ memory The in-process buffer. Proves the port, the vocabulary and the dedupe, and nothing
79
+ about a network.
80
+
81
+ posthog-stub The REAL posthog-node against a stub ingest endpoint this command starts on
82
+ loopback. Proves the adapter, the gzip, the batch and the uuid that carries the
83
+ dedupe \u2014 with no key and no internet.
84
+
85
+ posthog The real vendor, from POSTHOG_API_KEY and POSTHOG_HOST. Sending is not arriving, so
86
+ the read-back is --verify-command: it runs with GEONOSIS_TRACE_ID in its environment
87
+ and must exit 0 when it can see the trace id in your project. Without one this
88
+ refuses, because "the SDK did not complain" is not evidence.
89
+
90
+ Verdicts, and the exit code each carries:
91
+
92
+ PROVEN 0 the planted error was found, named by its trace id
93
+ CANNOT FAIL 2 the sink answered ok and kept nothing
94
+ MISREAD 2 the sink kept something, but not what was planted
95
+ CANNOT MEASURE 2 nothing was configured, or the plant never reached the sink at all`;
96
+ var SINKS = ["memory", "posthog", "posthog-stub"];
97
+ var VALUED = /* @__PURE__ */ new Set(["--sink", "--timeout", "--verify-command"]);
98
+ var parseProveArgs = (argv) => {
99
+ const read = {};
100
+ for (let index = 0; index < argv.length; index += 1) {
101
+ const flag = argv[index] ?? "";
102
+ const value = argv[index + 1];
103
+ if (!VALUED.has(flag)) throw new Error(`unknown argument "${flag}"`);
104
+ if (value === void 0 || value.startsWith("--")) throw new Error(`${flag} needs a value`);
105
+ read[flag] = value;
106
+ index += 1;
107
+ }
108
+ const sink = read["--sink"];
109
+ if (sink === void 0) throw new Error(`--sink is required \u2014 one of ${SINKS.join(", ")}`);
110
+ if (!SINKS.includes(sink)) {
111
+ throw new Error(`unknown sink "${sink}" \u2014 one of ${SINKS.join(", ")}`);
112
+ }
113
+ const seconds = Number(read["--timeout"] ?? "10");
114
+ if (!Number.isFinite(seconds) || seconds <= 0) {
115
+ throw new Error(`--timeout must be a positive number of seconds, not "${read["--timeout"]}"`);
116
+ }
117
+ return {
118
+ sink,
119
+ timeoutMs: seconds * 1e3,
120
+ ...read["--verify-command"] === void 0 ? {} : { verifyCommand: read["--verify-command"] }
121
+ };
122
+ };
123
+ var loadPostHog = async () => {
124
+ try {
125
+ const vendor = await import("posthog-node");
126
+ return vendor.PostHog;
127
+ } catch (cause) {
128
+ throw new Error(
129
+ `posthog-node is not installed here, so nothing can be proved against PostHog: ${cause.message}`,
130
+ { cause }
131
+ );
132
+ }
133
+ };
134
+ var posthogSink = async ({ apiKey, host }) => {
135
+ const PostHog = await loadPostHog();
136
+ return createPostHogSink({
137
+ newClient: (batchSize) => new PostHog(apiKey, {
138
+ disableGeoip: true,
139
+ // The probe's own answer must not be "it timed out retrying"; a refusal is the finding.
140
+ fetchRetryCount: 0,
141
+ flushAt: batchSize,
142
+ flushInterval: 0,
143
+ host
144
+ })
145
+ });
146
+ };
147
+ var stubProbe = async () => {
148
+ const stub = await startPostHogStub();
149
+ const sink = await posthogSink({ apiKey: "phc_geonosis_probe", host: stub.url });
150
+ return {
151
+ close: () => stub.close(),
152
+ kind: sink.kind,
153
+ seen: () => Promise.resolve(stub.events.map((one) => one.uuid).filter((one) => one !== void 0)),
154
+ sink
155
+ };
156
+ };
157
+ var vendorProbe = async ({
158
+ apiKey,
159
+ host,
160
+ verifyCommand
161
+ }) => {
162
+ const sink = await posthogSink({ apiKey, host });
163
+ return {
164
+ kind: sink.kind,
165
+ seen: (traceId) => Promise.resolve(seenByCommand(verifyCommand, traceId)),
166
+ sink
167
+ };
168
+ };
169
+ var seenByCommand = (command, traceId) => {
170
+ if (traceId === void 0) return [];
171
+ const done = spawnSync(command, {
172
+ encoding: "utf8",
173
+ env: { ...process.env, GEONOSIS_TRACE_ID: traceId },
174
+ shell: true,
175
+ stdio: ["ignore", "pipe", "pipe"]
176
+ });
177
+ return done.status === 0 ? [traceId] : [];
178
+ };
179
+ var probeFor = async (args) => {
180
+ if (args.sink === "memory") return memoryProbe();
181
+ if (args.sink === "posthog-stub") return stubProbe();
182
+ const apiKey = process.env.POSTHOG_API_KEY ?? "";
183
+ if (apiKey === "") return void 0;
184
+ const host = process.env.POSTHOG_HOST ?? "";
185
+ if (host === "") {
186
+ throw new Error(
187
+ "POSTHOG_API_KEY is set but POSTHOG_HOST is not \u2014 name the ingest host you mean rather than letting a probe pick one (eu.i.posthog.com and us.i.posthog.com are different projects)"
188
+ );
189
+ }
190
+ if (args.verifyCommand === void 0) {
191
+ throw new Error(
192
+ `a key is not a read-back: pass --verify-command <cmd>, which runs with GEONOSIS_TRACE_ID in its environment and must exit 0 when it can see that id in your project. Nothing in this kit queries ${host} on your behalf, and "the SDK did not complain" is the evidence this whole command exists because of`
193
+ );
194
+ }
195
+ return vendorProbe({ apiKey, host, verifyCommand: args.verifyCommand });
196
+ };
197
+ var proofFor = async (args) => {
198
+ try {
199
+ return await proveObservability({ probe: await probeFor(args), timeoutMs: args.timeoutMs });
200
+ } catch (cause) {
201
+ return { kind: args.sink, reason: cause.message, verdict: "cannot-measure" };
202
+ }
203
+ };
204
+ var main = async () => {
205
+ const argv = process.argv.slice(2);
206
+ if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
207
+ process.stdout.write(`${USAGE}
208
+ `);
209
+ return argv.length === 0 ? 2 : 0;
210
+ }
211
+ const [command, ...rest] = argv;
212
+ if (command !== "prove") {
213
+ throw new Error(`unknown command "${command}" \u2014 this bin has one, "prove". See --help.`);
214
+ }
215
+ const proof = await proofFor(parseProveArgs(rest));
216
+ process.stdout.write(`${formatProof(proof)}
217
+ `);
218
+ return exitCodeOf(proof);
219
+ };
220
+ try {
221
+ process.exit(await main());
222
+ } catch (error) {
223
+ process.stderr.write(`geonosis-observability: ${error.message}
224
+ `);
225
+ process.exit(2);
226
+ }
227
+ export {
228
+ parseProveArgs
229
+ };
@@ -0,0 +1,116 @@
1
+ import * as better_result from 'better-result';
2
+ import { Result } from 'better-result';
3
+
4
+ /**
5
+ * The ports.
6
+ *
7
+ * during.day's seam answers `Promise<number>` — `events.length` from the real sink, `0` from the
8
+ * one with no key, a rejected promise from a dead transport — and it is handed to `waitUntil`,
9
+ * where nothing reads it. A count cannot say *failed* and nobody was listening anyway. That is the
10
+ * shape `no-void-port` refuses, wearing a number instead of `void`.
11
+ *
12
+ * So the answer is a `Result` and it carries an ID. React strips an error's message on the way to a
13
+ * browser; a deploy renames every stack frame. The one thing that survives both is the id the sink
14
+ * filed the report under, which is why it is in the receipt rather than in a log line.
15
+ */
16
+ /** A value a vendor can put on the wire without a serializer of its own. */
17
+ type AttributeValue = boolean | null | number | string;
18
+ /**
19
+ * The vocabulary. Every field is one of these or it goes in `extra`, and `extra` may not shadow one
20
+ * of these — a `service` in the bag and a `service` on the envelope is a report with two answers.
21
+ */
22
+ type Attributes = {
23
+ /**
24
+ * The person or process that caused it, when there is one. This is the only field that MAY carry
25
+ * an identity; `tenant` may not.
26
+ */
27
+ actor?: string;
28
+ /**
29
+ * The id this occurrence already has upstream — a queue envelope, a job id, a request id. The
30
+ * dedupe key, and the id the sink files under when it is present.
31
+ */
32
+ envelopeId?: string;
33
+ /** Anything this vocabulary does not name. May not shadow a field it does. */
34
+ extra?: Record<string, AttributeValue>;
35
+ /**
36
+ * Which build. NEITHER consumer attaches one today, which is why a production error could not be
37
+ * pinned to a deploy — the first of the four lying instruments of `docs/rules-backlog.md` #42.
38
+ */
39
+ release?: string;
40
+ /** Where the code was running: `node`, `workerd`, `browser`, a container id. An open string. */
41
+ runtime?: string;
42
+ /** Which service emitted this. Required: a report nobody can route is a report nobody reads. */
43
+ service: string;
44
+ /**
45
+ * The tenant, ALWAYS a group id and never a person. It is the join key every dashboard fans out
46
+ * on and the field a GDPR erasure cannot reach once a person is in it.
47
+ */
48
+ tenant?: string;
49
+ };
50
+ /** What a sink answers with when it accepted a report. */
51
+ type CaptureReceipt = {
52
+ /**
53
+ * True when the sink recognised this envelope id and did not file it a second time. A capture
54
+ * that did less than it looks like it did says so, rather than being counted as another send.
55
+ */
56
+ deduplicated: boolean;
57
+ /** The id the sink filed it under — what a human greps the vendor for. */
58
+ id: string;
59
+ };
60
+ declare const SinkFailure_base: better_result.TaggedErrorClass<"SinkFailure">;
61
+ /**
62
+ * Why a sink could not take a report. A tagged error so a consumer can match it beside its own,
63
+ * and an `Error` so it still prints and still has a stack.
64
+ */
65
+ declare class SinkFailure extends SinkFailure_base<{
66
+ cause?: unknown;
67
+ message: string;
68
+ /** Which sink. A failure that cannot name itself sends the reader to the wrong file. */
69
+ sink: string;
70
+ }> {
71
+ }
72
+ type ErrorSink = {
73
+ capture: (error: Error, attributes: Attributes) => Promise<Result<CaptureReceipt, SinkFailure>>;
74
+ /**
75
+ * Send whatever is held. Explicit, always: there is no long-lived process to run a background
76
+ * timer in, and a timer left running is an isolate that will not shut down.
77
+ */
78
+ flush: () => Promise<Result<void, SinkFailure>>;
79
+ /** What this sink is, in a verdict. `memory`, `posthog`, `swallowing`, `memory+dedupe`. */
80
+ kind: string;
81
+ };
82
+ /** What a tracer recorded about one span. */
83
+ type SpanRecord = {
84
+ attributes: Attributes;
85
+ /** Whether the name follows `SPAN_NAME_CONVENTION`. Recorded, never enforced by a throw. */
86
+ conventional: boolean;
87
+ durationMs: number;
88
+ error?: Error;
89
+ name: string;
90
+ outcome: 'error' | 'ok';
91
+ };
92
+ /**
93
+ * `span` returns whatever the body returned and rethrows whatever it threw. It is a recorder, not a
94
+ * gate: a tracer that swallowed a throw would make a failed request answer 200, which is the third
95
+ * lying instrument of #42 rebuilt on purpose. Its own failures are its own business and never the
96
+ * caller's — the same trade during.day's `reportFailure` makes out loud.
97
+ */
98
+ type Tracer = {
99
+ kind: string;
100
+ span: <Value>(name: string, attributes: Attributes, body: () => Promise<Value>) => Promise<Value>;
101
+ };
102
+ /**
103
+ * The record a consumer writes somewhere `geonosis-doctor` can read it — a file on Node, a KV entry
104
+ * on workerd. The doctor parses this shape and never imports this package: a check that needed the
105
+ * library it checks is a check that cannot run where the library is missing, which is the case it
106
+ * exists to find.
107
+ */
108
+ type LastEventRecord = {
109
+ /** Epoch milliseconds. */
110
+ at: number;
111
+ id: string;
112
+ release?: string;
113
+ sink: string;
114
+ };
115
+
116
+ export { type Attributes as A, type CaptureReceipt as C, type ErrorSink as E, type LastEventRecord as L, type SpanRecord as S, type Tracer as T, type AttributeValue as a, SinkFailure as b };