/** * One data point per connector call (#1691) — the trend behind the connection-health * line. * * The health line (`recordConnectionUse`, connections.md §3.7) is last-write-wins: it * answers "what happened last" and nothing about "is this provider getting worse". This * is the other half: every time either host settles that line it also hands a * {@link ConnectorCallRecord} to a {@link ConnectorCallRecorder}, which on the hosted * platform is an Analytics Engine dataset and on self-host is, by default, nothing. * * ## Why the record cannot carry a secret * * The shape is closed, not filtered. Every field is either an identifier the host read * off the connection row (provider, tenant, vertical — never the caller's input), a member * of the closed {@link CONNECTOR_CALL_ERROR_TYPES} enum, or a number. There is no field a * URL, a header, a request body, a credential or an error message could land in, because * the classifier ({@link connectorCallErrorType}) reads only a status code and an error's * `name`, and emits an enum member. A free-text "error class" would have been the one * door a provider's error body — which routinely echoes the request — walks through. For * the same reason there is deliberately no `server.address` and no `url.*`: a URL is * where a provider puts an access token. * * ## The names are OpenTelemetry's * * The record is keyed by OpenTelemetry semantic-convention names (checked against * `@opentelemetry/semantic-conventions` 1.43.0, where all three standard names below are * STABLE), with a `substrat.*` namespace for the fields OTel has no name for. So this * Analytics Engine row, a future custom span, and a future self-host OTel metric share one * vocabulary, and an OTLP exporter maps the record 1:1 — including the duration's unit, * which is SECONDS, OTel's unit for `http.client.request.duration`. * * ## Why it cannot fail or slow the call * * `record` returns `void` and the hosts never await it. The Analytics Engine recorder * (`@substrat-run/adapter-cloudflare`) wraps its write and counts what it swallows * ({@link CountingConnectorCallRecorder}), and the hosts wrap the call itself too, so a recorder someone else wrote cannot break a * dispatch by throwing either. */ /** * The `error.type` values this instrumentation reports — closed, as OTel asks ("low * cardinality … instrumentations SHOULD document the list of errors they report"). * Grow-only like the data point's ordinals: a stored point keeps the string it was written with, * so a member is never renamed or reused. A SUCCESS has no `error.type` at all (OTel: * "SHOULD NOT set `error.type`" on success), which the data point writes as `''`. * * - `4xx` / `5xx` — the provider answered with that class. A 4xx is usually us (a revoked * grant, a malformed call); a 5xx is usually them. The exact code is * `http.response.status_code`, OTel's domain-specific attribute beside it. * - `other_status` — a non-ok status outside 4xx/5xx (an unfollowed 3xx, say). * - `timeout` — the call was aborted by the host's timeout (OTel's own example value). * - `network` — the call threw before any status arrived. * - `_OTHER` — OTel's fallback value: an error recorded without either of the facts above * (a caller that only knew "it failed"). Counted as an error; never guessed into a * better class. */ export declare const CONNECTOR_CALL_ERROR_TYPES: readonly ['4xx', '5xx', 'other_status', 'timeout', 'network', '_OTHER']; export type ConnectorCallErrorType = (typeof CONNECTOR_CALL_ERROR_TYPES)[number]; /** * What a connector call's settlement may say about itself, beyond ok/error: how long it * took and what status it met. Optional on `recordConnectionUse` so a caller that knows * neither still records the health line exactly as before. */ export interface ConnectionUseTiming { /** Wall time of the outbound call, milliseconds. */ durationMs?: number; /** The provider's HTTP status, when one arrived. */ status?: number; /** The call was aborted by the host's timeout rather than failing on its own. */ timedOut?: boolean; } export type ConnectionUseOutcome = ({ ok: true; } & ConnectionUseTiming) | ({ ok: false; error: string; } & ConnectionUseTiming); /** * One call, as the recorder receives it — keyed by OpenTelemetry names (see the module * header). Every field is closed or read off the connection row. */ export interface ConnectorCallRecord { /** The tenant's id (a ULID), off the connection row. */ 'substrat.tenant.id': string; /** The vertical's SLUG (e.g. `callout`) — the connection row's namespace, not an id. */ 'substrat.vertical': string; /** The provider slug the connection is for (`scrive`, `fortnox`, …). */ 'substrat.connection.provider': string; /** OTel `error.type`: absent on success, else one of {@link CONNECTOR_CALL_ERROR_TYPES}. */ 'error.type'?: ConnectorCallErrorType; /** OTel `http.response.status_code`: absent when no status arrived. */ 'http.response.status_code'?: number; /** * OTel `http.client.request.duration`, in SECONDS (OTel's unit for it). Absent when the * caller did not time the call — the data point writes that as `-1`. */ 'http.client.request.duration'?: number; } export interface ConnectorCallRecorder { /** Fire-and-forget. The hosts never await it, and swallow a throw. */ record(call: ConnectorCallRecord): void; } /** The self-host default: the shape exists, nothing is written. */ export declare const noopConnectorCallRecorder: ConnectorCallRecorder; /** * The settlement a connection's `fetch` wrapper hands `recordConnectionUse` — the error * strings are exactly the ones the health line always carried, plus the timing facts the * recorder classifies from. One spelling, so the five wrappers (both hosts, three * connectors) cannot disagree about what a timeout is. */ export declare function settleConnectionUse(provider: string, durationMs: number, settled: { response: { ok: boolean; status: number; }; } | { error: unknown; }): ConnectionUseOutcome; /** Build the record from the row's identity and the settlement. */ export declare function connectorCallRecord(row: { tenantId: string; vertical: string; provider: string; }, outcome: ConnectionUseOutcome): ConnectorCallRecord; /** Hand a record to a recorder without letting it throw into the caller. */ export declare function recordConnectorCall(recorder: ConnectorCallRecorder, call: ConnectorCallRecord): void; /** A recorder that also says how many writes it swallowed. */ export interface CountingConnectorCallRecorder extends ConnectorCallRecorder { /** Writes that threw and were dropped, since this recorder was built. */ readonly dropped: number; } //# sourceMappingURL=connector-calls.d.ts.map