@glasstrace/sdk 1.17.0 → 1.18.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.
- package/dist/{capture-error-B0txjNut.d.cts → capture-error-B8qiXFeC.d.cts} +2 -2
- package/dist/{capture-error-Dc01rYNR.d.ts → capture-error-BTI6mCH2.d.ts} +2 -2
- package/dist/{chunk-VEQX2YSQ.js → chunk-SONZOTBP.js} +9 -1
- package/dist/{chunk-VEQX2YSQ.js.map → chunk-SONZOTBP.js.map} +1 -1
- package/dist/{chunk-F2IPBTDJ.js → chunk-Z2DSC3YI.js} +20 -8
- package/dist/{chunk-F2IPBTDJ.js.map → chunk-Z2DSC3YI.js.map} +1 -1
- package/dist/cli/init.cjs +4 -4
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.js +3 -3
- package/dist/cli/mcp-add.cjs +1 -1
- package/dist/cli/mcp-add.js +1 -1
- package/dist/cli/upgrade-instructions.cjs +1 -1
- package/dist/cli/upgrade-instructions.js +1 -1
- package/dist/{correlation-id-CelUvw7j.d.cts → correlation-id-CClOq8Wn.d.cts} +1 -1
- package/dist/{correlation-id-B9YYmoZw.d.ts → correlation-id-Ct86Ug4s.d.ts} +1 -1
- package/dist/edge-entry.d.cts +2 -2
- package/dist/edge-entry.d.ts +2 -2
- package/dist/{import-graph-Dka_Fm7j.d.ts → import-graph-DZjTJdJ5.d.ts} +1 -1
- package/dist/{import-graph-DBLGNjcI.d.cts → import-graph-DyQfZU2f.d.cts} +1 -1
- package/dist/index.cjs +136 -6
- package/dist/index.cjs.map +1 -1
- package/dist/{index.d-3-cJoY8y.d.cts → index.d-DhatN7mq.d.cts} +1 -1
- package/dist/{index.d-3-cJoY8y.d.ts → index.d-DhatN7mq.d.ts} +1 -1
- package/dist/index.d.cts +170 -5
- package/dist/index.d.ts +170 -5
- package/dist/index.js +123 -3
- package/dist/index.js.map +1 -1
- package/dist/node-entry.cjs +5 -5
- package/dist/node-entry.cjs.map +1 -1
- package/dist/node-entry.d.cts +4 -4
- package/dist/node-entry.d.ts +4 -4
- package/dist/node-entry.js +2 -2
- package/dist/node-subpath.d.cts +2 -2
- package/dist/node-subpath.d.ts +2 -2
- package/package.json +1 -1
|
@@ -324,4 +324,4 @@ declare const SIDE_EFFECT_OPERATION_PHASES: readonly ["request", "post_response"
|
|
|
324
324
|
*/
|
|
325
325
|
type SideEffectOperationPhase = (typeof SIDE_EFFECT_OPERATION_PHASES)[number];
|
|
326
326
|
|
|
327
|
-
export { type AnonApiKey as A, type CaptureConfig as C, type GlasstraceEnvVars as G, type ImportGraphPayload as I, type
|
|
327
|
+
export { type AnonApiKey as A, type CaptureConfig as C, type GlasstraceEnvVars as G, type ImportGraphPayload as I, type SourceMapUploadResponse as S, type SourceMapManifestResponse as a, type SideEffectOperationKind as b, type SideEffectOperationStatus as c, deriveSessionId as d, type SideEffectOperationPhase as e, type SideEffectSemanticFieldKey as f, type SideEffectOmissionReason as g, type SideEffectSemanticFieldStableCoreKey as h, isSideEffectSemanticFieldKey as i, type SdkDiagnosticCode as j, type SessionId as k, type GlasstraceOptions as l, type SdkInitResponse as m, type SdkHealthReport as n };
|
|
@@ -324,4 +324,4 @@ declare const SIDE_EFFECT_OPERATION_PHASES: readonly ["request", "post_response"
|
|
|
324
324
|
*/
|
|
325
325
|
type SideEffectOperationPhase = (typeof SIDE_EFFECT_OPERATION_PHASES)[number];
|
|
326
326
|
|
|
327
|
-
export { type AnonApiKey as A, type CaptureConfig as C, type GlasstraceEnvVars as G, type ImportGraphPayload as I, type
|
|
327
|
+
export { type AnonApiKey as A, type CaptureConfig as C, type GlasstraceEnvVars as G, type ImportGraphPayload as I, type SourceMapUploadResponse as S, type SourceMapManifestResponse as a, type SideEffectOperationKind as b, type SideEffectOperationStatus as c, deriveSessionId as d, type SideEffectOperationPhase as e, type SideEffectSemanticFieldKey as f, type SideEffectOmissionReason as g, type SideEffectSemanticFieldStableCoreKey as h, isSideEffectSemanticFieldKey as i, type SdkDiagnosticCode as j, type SessionId as k, type GlasstraceOptions as l, type SdkInitResponse as m, type SdkHealthReport as n };
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export { C as CorrelationIdRequest, G as GlasstraceSpanProcessor, S as SdkError, a as SessionManager, c as captureCorrelationId, g as getDateString, b as getOrigin } from './correlation-id-
|
|
2
|
-
export { F as FetchTarget, G as GlasstraceExporter, a as GlasstraceExporterOptions, I as InitClaimResult, R as ResolvedConfig, c as captureError, b as classifyFetchTarget, d as createGlasstraceSpanProcessor, g as getActiveConfig, e as getDiscoveryHandler, f as getLinkedAccountId, h as getOrCreateAnonKey, i as getStatus, j as isAnonymousMode, k as isProductionDisabled, l as isReady, m as loadCachedConfig, p as performInit, r as readAnonKey, n as readEnvVars, o as registerGlasstrace, q as resolveConfig, s as saveCachedConfig, t as sendInitRequest, w as waitForReady, u as withGlasstraceConfig } from './capture-error-
|
|
3
|
-
import {
|
|
4
|
-
export {
|
|
1
|
+
export { C as CorrelationIdRequest, G as GlasstraceSpanProcessor, S as SdkError, a as SessionManager, c as captureCorrelationId, g as getDateString, b as getOrigin } from './correlation-id-CClOq8Wn.cjs';
|
|
2
|
+
export { F as FetchTarget, G as GlasstraceExporter, a as GlasstraceExporterOptions, I as InitClaimResult, R as ResolvedConfig, c as captureError, b as classifyFetchTarget, d as createGlasstraceSpanProcessor, g as getActiveConfig, e as getDiscoveryHandler, f as getLinkedAccountId, h as getOrCreateAnonKey, i as getStatus, j as isAnonymousMode, k as isProductionDisabled, l as isReady, m as loadCachedConfig, p as performInit, r as readAnonKey, n as readEnvVars, o as registerGlasstrace, q as resolveConfig, s as saveCachedConfig, t as sendInitRequest, w as waitForReady, u as withGlasstraceConfig } from './capture-error-B8qiXFeC.cjs';
|
|
3
|
+
import { b as SideEffectOperationKind, c as SideEffectOperationStatus, e as SideEffectOperationPhase, f as SideEffectSemanticFieldKey } from './index.d-DhatN7mq.cjs';
|
|
4
|
+
export { g as SideEffectOmissionReason, h as SideEffectSemanticFieldStableCoreKey, d as deriveSessionId, i as isSideEffectSemanticFieldKey } from './index.d-DhatN7mq.cjs';
|
|
5
|
+
import { Span } from './trace/span';
|
|
5
6
|
export { RequestMiddlewareFunction, TracedRequestMiddlewareOptions } from './middleware/index.cjs';
|
|
6
7
|
export { WithAsyncCausalityOptions } from './async-context/index.cjs';
|
|
7
8
|
import './export/ReadableSpan';
|
|
@@ -157,6 +158,170 @@ interface RecordSideEffectInput {
|
|
|
157
158
|
*/
|
|
158
159
|
declare function recordSideEffect(input: RecordSideEffectInput): void;
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Public value-capture primitive (L1 passive capture).
|
|
163
|
+
*
|
|
164
|
+
* {@link capture} emits a single allowlisted value-fidelity scalar onto a
|
|
165
|
+
* caller-**owned** OTel span. It is the counterpart to {@link
|
|
166
|
+
* recordSideEffect} for the case where the emitter owns the target span
|
|
167
|
+
* itself (e.g. a passive database adapter that opens a `db.<Model>.<op>`
|
|
168
|
+
* span) rather than attaching to the ambient active span.
|
|
169
|
+
*
|
|
170
|
+
* Why this exists: {@link recordSideEffect} is ambient-only — it resolves
|
|
171
|
+
* the active span via `getActiveSpan()`. A passive adapter cannot use it,
|
|
172
|
+
* because at its capture point the ambient span is the database client's
|
|
173
|
+
* own operation span, which has already ended and is non-recording. So the
|
|
174
|
+
* adapter must own a fresh recording span and emit onto it explicitly.
|
|
175
|
+
*
|
|
176
|
+
* The behavior contract mirrors {@link recordSideEffect}: observational
|
|
177
|
+
* only. `capture` never executes a side effect, never reads or mutates the
|
|
178
|
+
* captured value's source, and **never throws**. Every failure mode
|
|
179
|
+
* (capture-config disabled, ended / `NonRecordingSpan` target, allowlist
|
|
180
|
+
* rejection, OTel attribute-slot exhaustion) routes to a silent no-op or to
|
|
181
|
+
* an omission-counter increment that carries no rejected input.
|
|
182
|
+
*
|
|
183
|
+
* Capture is **strict-mode only**: timestamp-shaped and unhashed-identifier
|
|
184
|
+
* values are rejected at emit so they never reach the wire. The `full`
|
|
185
|
+
* fidelity relaxation is not reachable through this primitive.
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
/** Options for {@link capture}. */
|
|
189
|
+
interface CaptureOptions {
|
|
190
|
+
/**
|
|
191
|
+
* The caller-owned, recording span to attach the scalar to. `capture`
|
|
192
|
+
* writes only to this span; it never resolves or touches the ambient
|
|
193
|
+
* active span. The caller is responsible for `end()`-ing it.
|
|
194
|
+
*/
|
|
195
|
+
span: Span;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Emit a single allowlisted value-fidelity scalar onto a caller-owned span.
|
|
199
|
+
*
|
|
200
|
+
* The scalar `name` must match the value-fidelity scalar key pattern
|
|
201
|
+
* (`*Ms` / `*Amount` / `*Bytes` / `*Ratio` / `*Id` / `*Value` / `*Flag`)
|
|
202
|
+
* and its value must match the suffix type — e.g. a `*Flag` key requires a
|
|
203
|
+
* `boolean`. Mismatches, timestamp-shaped values, and unhashed `*Id`s are
|
|
204
|
+
* rejected under strict mode and recorded as an omission count on the
|
|
205
|
+
* supplied span (never the active span).
|
|
206
|
+
*
|
|
207
|
+
* Edge cases (all silent no-ops, never throws):
|
|
208
|
+
* - capture-config flag `sideEffectEvidence` is `false` ⇒ no-op, **no
|
|
209
|
+
* counter** (mirrors {@link recordSideEffect}: with capture disabled the
|
|
210
|
+
* SDK does no allowlist evaluation and writes nothing).
|
|
211
|
+
* - the supplied span has already ended or is a `NonRecordingSpan` ⇒
|
|
212
|
+
* no-op, **no counter** (its omission counter would itself be a dropped
|
|
213
|
+
* span write). The owning adapter passes a fresh recording span, so this
|
|
214
|
+
* is a caller-misuse guard.
|
|
215
|
+
* - the value fails strict allowlist validation ⇒ an omission count is
|
|
216
|
+
* recorded on the supplied span; the rejected value is never emitted.
|
|
217
|
+
* - OTel attribute-slot exhaustion ⇒ the attribute write is silently
|
|
218
|
+
* dropped.
|
|
219
|
+
*
|
|
220
|
+
* @example Project a boolean result field onto an owned database span
|
|
221
|
+
* ```ts
|
|
222
|
+
* import { capture } from "@glasstrace/sdk";
|
|
223
|
+
*
|
|
224
|
+
* const span = tracer.startSpan("db.Poll.findUnique");
|
|
225
|
+
* try {
|
|
226
|
+
* const row = await query(args);
|
|
227
|
+
* if (row) capture("mutedFlag", row.muted, { span });
|
|
228
|
+
* return row;
|
|
229
|
+
* } finally {
|
|
230
|
+
* span.end();
|
|
231
|
+
* }
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
declare function capture(name: string, value: unknown, options: CaptureOptions): void;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Passive Prisma value-capture adapter (L1 capture).
|
|
238
|
+
*
|
|
239
|
+
* `prismaAdapter({ allow })` returns a Prisma client extension that, for
|
|
240
|
+
* each allowlisted `(model, column)`, projects a boolean result field onto
|
|
241
|
+
* a Glasstrace value-fidelity scalar so an agent can read it back from the
|
|
242
|
+
* trace. It is **passive and observational**: it never executes a query
|
|
243
|
+
* itself, never reads or mutates the result, and never changes query
|
|
244
|
+
* behavior or errors.
|
|
245
|
+
*
|
|
246
|
+
* Apply it like any Prisma extension:
|
|
247
|
+
*
|
|
248
|
+
* ```ts
|
|
249
|
+
* import { prismaAdapter } from "@glasstrace/sdk";
|
|
250
|
+
*
|
|
251
|
+
* const prisma = new PrismaClient().$extends(
|
|
252
|
+
* prismaAdapter({ allow: [{ model: "Poll", column: "muted" }] }),
|
|
253
|
+
* );
|
|
254
|
+
* ```
|
|
255
|
+
*
|
|
256
|
+
* Design:
|
|
257
|
+
* - **OWN a span.** At the capture point the database client's own
|
|
258
|
+
* operation span has already ended, so the adapter opens its own
|
|
259
|
+
* recording `db.<Model>.<op>` span (parented under the active request
|
|
260
|
+
* span) and emits onto it via {@link capture}.
|
|
261
|
+
* - **Default-deny.** Nothing is captured unless an explicit `allow` entry
|
|
262
|
+
* matches AND the server-pushed `sideEffectEvidence` capture flag is on.
|
|
263
|
+
* An empty / unset `allow` captures nothing.
|
|
264
|
+
* - **Boolean only.** This adapter projects boolean columns (the strict,
|
|
265
|
+
* no-`captureFidelity:full` case). Non-boolean allowlisted columns route
|
|
266
|
+
* to a safe omission counter, never a captured value.
|
|
267
|
+
* - **Pure observer.** Capture work can never throw into the host query;
|
|
268
|
+
* the owned span is always ended; the original query error is re-thrown
|
|
269
|
+
* verbatim.
|
|
270
|
+
* - **Bounded.** `findMany` / list operations are disabled (no per-row
|
|
271
|
+
* capture). The adapter never widens the app's `select`.
|
|
272
|
+
*
|
|
273
|
+
* This module has **no dependency on `@prisma/client`** — it is typed
|
|
274
|
+
* structurally against Prisma's client-extension shape (mirroring the
|
|
275
|
+
* Drizzle adapter), so it adds no runtime dependency and ships on the edge-
|
|
276
|
+
* safe root barrel. On a runtime with no active request span (e.g. an edge
|
|
277
|
+
* runtime with no AsyncLocalStorage), it captures nothing.
|
|
278
|
+
*/
|
|
279
|
+
/** The arguments Prisma passes to a `$allOperations` query-extension callback. */
|
|
280
|
+
interface PrismaAllOperationsArgs {
|
|
281
|
+
/** The Prisma model name (PascalCase, e.g. `Poll`), or `undefined` for raw ops. */
|
|
282
|
+
model?: string;
|
|
283
|
+
/** The Prisma operation (e.g. `findUnique`, `findMany`, `update`). */
|
|
284
|
+
operation: string;
|
|
285
|
+
/** The operation arguments, forwarded unchanged to `query`. */
|
|
286
|
+
args: unknown;
|
|
287
|
+
/** Executes the underlying operation. Called exactly once. */
|
|
288
|
+
query: (args: unknown) => Promise<unknown>;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* A Prisma client extension — the object passed to `prisma.$extends(...)`.
|
|
292
|
+
* Structurally typed so the adapter needs no `@prisma/client` dependency.
|
|
293
|
+
*/
|
|
294
|
+
interface PrismaCaptureExtension {
|
|
295
|
+
name: string;
|
|
296
|
+
query: {
|
|
297
|
+
$allModels: {
|
|
298
|
+
$allOperations(args: PrismaAllOperationsArgs): Promise<unknown>;
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/** A single allowlisted column to project. */
|
|
303
|
+
interface PrismaCaptureColumn {
|
|
304
|
+
/** The Prisma model name, PascalCase, exactly as Prisma reports it (e.g. `Poll`). */
|
|
305
|
+
model: string;
|
|
306
|
+
/** The boolean result column to project (e.g. `muted`). */
|
|
307
|
+
column: string;
|
|
308
|
+
}
|
|
309
|
+
/** Options for {@link prismaAdapter}. */
|
|
310
|
+
interface PrismaAdapterOptions {
|
|
311
|
+
/**
|
|
312
|
+
* The default-deny allowlist. Only `(model, column)` pairs listed here are
|
|
313
|
+
* eligible for capture; an empty or unset list captures nothing. The
|
|
314
|
+
* server-side per-tenant allowlist re-enforces this independently at
|
|
315
|
+
* ingestion.
|
|
316
|
+
*/
|
|
317
|
+
allow?: ReadonlyArray<PrismaCaptureColumn>;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Build a passive Prisma value-capture extension. See the module doc for
|
|
321
|
+
* the full behavior contract.
|
|
322
|
+
*/
|
|
323
|
+
declare function prismaAdapter(options?: PrismaAdapterOptions): PrismaCaptureExtension;
|
|
324
|
+
|
|
160
325
|
/**
|
|
161
326
|
* Producer-sugar for computing boolean relations to emit as `*Holds`
|
|
162
327
|
* side-effect evidence.
|
|
@@ -216,4 +381,4 @@ declare function invariant<T extends number | string | bigint | boolean>(left: T
|
|
|
216
381
|
*/
|
|
217
382
|
declare function isNullInvariant(value: unknown): boolean;
|
|
218
383
|
|
|
219
|
-
export { type InvariantOp, type RecordSideEffectInput, SideEffectOperationKind, SideEffectOperationPhase, SideEffectOperationStatus, SideEffectSemanticFieldKey, invariant, isNullInvariant, recordSideEffect };
|
|
384
|
+
export { type CaptureOptions, type InvariantOp, type PrismaAdapterOptions, type PrismaCaptureColumn, type PrismaCaptureExtension, type RecordSideEffectInput, SideEffectOperationKind, SideEffectOperationPhase, SideEffectOperationStatus, SideEffectSemanticFieldKey, capture, invariant, isNullInvariant, prismaAdapter, recordSideEffect };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export { C as CorrelationIdRequest, G as GlasstraceSpanProcessor, S as SdkError, a as SessionManager, c as captureCorrelationId, g as getDateString, b as getOrigin } from './correlation-id-
|
|
2
|
-
export { F as FetchTarget, G as GlasstraceExporter, a as GlasstraceExporterOptions, I as InitClaimResult, R as ResolvedConfig, c as captureError, b as classifyFetchTarget, d as createGlasstraceSpanProcessor, g as getActiveConfig, e as getDiscoveryHandler, f as getLinkedAccountId, h as getOrCreateAnonKey, i as getStatus, j as isAnonymousMode, k as isProductionDisabled, l as isReady, m as loadCachedConfig, p as performInit, r as readAnonKey, n as readEnvVars, o as registerGlasstrace, q as resolveConfig, s as saveCachedConfig, t as sendInitRequest, w as waitForReady, u as withGlasstraceConfig } from './capture-error-
|
|
3
|
-
import {
|
|
4
|
-
export {
|
|
1
|
+
export { C as CorrelationIdRequest, G as GlasstraceSpanProcessor, S as SdkError, a as SessionManager, c as captureCorrelationId, g as getDateString, b as getOrigin } from './correlation-id-Ct86Ug4s.js';
|
|
2
|
+
export { F as FetchTarget, G as GlasstraceExporter, a as GlasstraceExporterOptions, I as InitClaimResult, R as ResolvedConfig, c as captureError, b as classifyFetchTarget, d as createGlasstraceSpanProcessor, g as getActiveConfig, e as getDiscoveryHandler, f as getLinkedAccountId, h as getOrCreateAnonKey, i as getStatus, j as isAnonymousMode, k as isProductionDisabled, l as isReady, m as loadCachedConfig, p as performInit, r as readAnonKey, n as readEnvVars, o as registerGlasstrace, q as resolveConfig, s as saveCachedConfig, t as sendInitRequest, w as waitForReady, u as withGlasstraceConfig } from './capture-error-BTI6mCH2.js';
|
|
3
|
+
import { b as SideEffectOperationKind, c as SideEffectOperationStatus, e as SideEffectOperationPhase, f as SideEffectSemanticFieldKey } from './index.d-DhatN7mq.js';
|
|
4
|
+
export { g as SideEffectOmissionReason, h as SideEffectSemanticFieldStableCoreKey, d as deriveSessionId, i as isSideEffectSemanticFieldKey } from './index.d-DhatN7mq.js';
|
|
5
|
+
import { Span } from './trace/span';
|
|
5
6
|
export { RequestMiddlewareFunction, TracedRequestMiddlewareOptions } from './middleware/index.js';
|
|
6
7
|
export { WithAsyncCausalityOptions } from './async-context/index.js';
|
|
7
8
|
import './export/ReadableSpan';
|
|
@@ -157,6 +158,170 @@ interface RecordSideEffectInput {
|
|
|
157
158
|
*/
|
|
158
159
|
declare function recordSideEffect(input: RecordSideEffectInput): void;
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Public value-capture primitive (L1 passive capture).
|
|
163
|
+
*
|
|
164
|
+
* {@link capture} emits a single allowlisted value-fidelity scalar onto a
|
|
165
|
+
* caller-**owned** OTel span. It is the counterpart to {@link
|
|
166
|
+
* recordSideEffect} for the case where the emitter owns the target span
|
|
167
|
+
* itself (e.g. a passive database adapter that opens a `db.<Model>.<op>`
|
|
168
|
+
* span) rather than attaching to the ambient active span.
|
|
169
|
+
*
|
|
170
|
+
* Why this exists: {@link recordSideEffect} is ambient-only — it resolves
|
|
171
|
+
* the active span via `getActiveSpan()`. A passive adapter cannot use it,
|
|
172
|
+
* because at its capture point the ambient span is the database client's
|
|
173
|
+
* own operation span, which has already ended and is non-recording. So the
|
|
174
|
+
* adapter must own a fresh recording span and emit onto it explicitly.
|
|
175
|
+
*
|
|
176
|
+
* The behavior contract mirrors {@link recordSideEffect}: observational
|
|
177
|
+
* only. `capture` never executes a side effect, never reads or mutates the
|
|
178
|
+
* captured value's source, and **never throws**. Every failure mode
|
|
179
|
+
* (capture-config disabled, ended / `NonRecordingSpan` target, allowlist
|
|
180
|
+
* rejection, OTel attribute-slot exhaustion) routes to a silent no-op or to
|
|
181
|
+
* an omission-counter increment that carries no rejected input.
|
|
182
|
+
*
|
|
183
|
+
* Capture is **strict-mode only**: timestamp-shaped and unhashed-identifier
|
|
184
|
+
* values are rejected at emit so they never reach the wire. The `full`
|
|
185
|
+
* fidelity relaxation is not reachable through this primitive.
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
/** Options for {@link capture}. */
|
|
189
|
+
interface CaptureOptions {
|
|
190
|
+
/**
|
|
191
|
+
* The caller-owned, recording span to attach the scalar to. `capture`
|
|
192
|
+
* writes only to this span; it never resolves or touches the ambient
|
|
193
|
+
* active span. The caller is responsible for `end()`-ing it.
|
|
194
|
+
*/
|
|
195
|
+
span: Span;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Emit a single allowlisted value-fidelity scalar onto a caller-owned span.
|
|
199
|
+
*
|
|
200
|
+
* The scalar `name` must match the value-fidelity scalar key pattern
|
|
201
|
+
* (`*Ms` / `*Amount` / `*Bytes` / `*Ratio` / `*Id` / `*Value` / `*Flag`)
|
|
202
|
+
* and its value must match the suffix type — e.g. a `*Flag` key requires a
|
|
203
|
+
* `boolean`. Mismatches, timestamp-shaped values, and unhashed `*Id`s are
|
|
204
|
+
* rejected under strict mode and recorded as an omission count on the
|
|
205
|
+
* supplied span (never the active span).
|
|
206
|
+
*
|
|
207
|
+
* Edge cases (all silent no-ops, never throws):
|
|
208
|
+
* - capture-config flag `sideEffectEvidence` is `false` ⇒ no-op, **no
|
|
209
|
+
* counter** (mirrors {@link recordSideEffect}: with capture disabled the
|
|
210
|
+
* SDK does no allowlist evaluation and writes nothing).
|
|
211
|
+
* - the supplied span has already ended or is a `NonRecordingSpan` ⇒
|
|
212
|
+
* no-op, **no counter** (its omission counter would itself be a dropped
|
|
213
|
+
* span write). The owning adapter passes a fresh recording span, so this
|
|
214
|
+
* is a caller-misuse guard.
|
|
215
|
+
* - the value fails strict allowlist validation ⇒ an omission count is
|
|
216
|
+
* recorded on the supplied span; the rejected value is never emitted.
|
|
217
|
+
* - OTel attribute-slot exhaustion ⇒ the attribute write is silently
|
|
218
|
+
* dropped.
|
|
219
|
+
*
|
|
220
|
+
* @example Project a boolean result field onto an owned database span
|
|
221
|
+
* ```ts
|
|
222
|
+
* import { capture } from "@glasstrace/sdk";
|
|
223
|
+
*
|
|
224
|
+
* const span = tracer.startSpan("db.Poll.findUnique");
|
|
225
|
+
* try {
|
|
226
|
+
* const row = await query(args);
|
|
227
|
+
* if (row) capture("mutedFlag", row.muted, { span });
|
|
228
|
+
* return row;
|
|
229
|
+
* } finally {
|
|
230
|
+
* span.end();
|
|
231
|
+
* }
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
declare function capture(name: string, value: unknown, options: CaptureOptions): void;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Passive Prisma value-capture adapter (L1 capture).
|
|
238
|
+
*
|
|
239
|
+
* `prismaAdapter({ allow })` returns a Prisma client extension that, for
|
|
240
|
+
* each allowlisted `(model, column)`, projects a boolean result field onto
|
|
241
|
+
* a Glasstrace value-fidelity scalar so an agent can read it back from the
|
|
242
|
+
* trace. It is **passive and observational**: it never executes a query
|
|
243
|
+
* itself, never reads or mutates the result, and never changes query
|
|
244
|
+
* behavior or errors.
|
|
245
|
+
*
|
|
246
|
+
* Apply it like any Prisma extension:
|
|
247
|
+
*
|
|
248
|
+
* ```ts
|
|
249
|
+
* import { prismaAdapter } from "@glasstrace/sdk";
|
|
250
|
+
*
|
|
251
|
+
* const prisma = new PrismaClient().$extends(
|
|
252
|
+
* prismaAdapter({ allow: [{ model: "Poll", column: "muted" }] }),
|
|
253
|
+
* );
|
|
254
|
+
* ```
|
|
255
|
+
*
|
|
256
|
+
* Design:
|
|
257
|
+
* - **OWN a span.** At the capture point the database client's own
|
|
258
|
+
* operation span has already ended, so the adapter opens its own
|
|
259
|
+
* recording `db.<Model>.<op>` span (parented under the active request
|
|
260
|
+
* span) and emits onto it via {@link capture}.
|
|
261
|
+
* - **Default-deny.** Nothing is captured unless an explicit `allow` entry
|
|
262
|
+
* matches AND the server-pushed `sideEffectEvidence` capture flag is on.
|
|
263
|
+
* An empty / unset `allow` captures nothing.
|
|
264
|
+
* - **Boolean only.** This adapter projects boolean columns (the strict,
|
|
265
|
+
* no-`captureFidelity:full` case). Non-boolean allowlisted columns route
|
|
266
|
+
* to a safe omission counter, never a captured value.
|
|
267
|
+
* - **Pure observer.** Capture work can never throw into the host query;
|
|
268
|
+
* the owned span is always ended; the original query error is re-thrown
|
|
269
|
+
* verbatim.
|
|
270
|
+
* - **Bounded.** `findMany` / list operations are disabled (no per-row
|
|
271
|
+
* capture). The adapter never widens the app's `select`.
|
|
272
|
+
*
|
|
273
|
+
* This module has **no dependency on `@prisma/client`** — it is typed
|
|
274
|
+
* structurally against Prisma's client-extension shape (mirroring the
|
|
275
|
+
* Drizzle adapter), so it adds no runtime dependency and ships on the edge-
|
|
276
|
+
* safe root barrel. On a runtime with no active request span (e.g. an edge
|
|
277
|
+
* runtime with no AsyncLocalStorage), it captures nothing.
|
|
278
|
+
*/
|
|
279
|
+
/** The arguments Prisma passes to a `$allOperations` query-extension callback. */
|
|
280
|
+
interface PrismaAllOperationsArgs {
|
|
281
|
+
/** The Prisma model name (PascalCase, e.g. `Poll`), or `undefined` for raw ops. */
|
|
282
|
+
model?: string;
|
|
283
|
+
/** The Prisma operation (e.g. `findUnique`, `findMany`, `update`). */
|
|
284
|
+
operation: string;
|
|
285
|
+
/** The operation arguments, forwarded unchanged to `query`. */
|
|
286
|
+
args: unknown;
|
|
287
|
+
/** Executes the underlying operation. Called exactly once. */
|
|
288
|
+
query: (args: unknown) => Promise<unknown>;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* A Prisma client extension — the object passed to `prisma.$extends(...)`.
|
|
292
|
+
* Structurally typed so the adapter needs no `@prisma/client` dependency.
|
|
293
|
+
*/
|
|
294
|
+
interface PrismaCaptureExtension {
|
|
295
|
+
name: string;
|
|
296
|
+
query: {
|
|
297
|
+
$allModels: {
|
|
298
|
+
$allOperations(args: PrismaAllOperationsArgs): Promise<unknown>;
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/** A single allowlisted column to project. */
|
|
303
|
+
interface PrismaCaptureColumn {
|
|
304
|
+
/** The Prisma model name, PascalCase, exactly as Prisma reports it (e.g. `Poll`). */
|
|
305
|
+
model: string;
|
|
306
|
+
/** The boolean result column to project (e.g. `muted`). */
|
|
307
|
+
column: string;
|
|
308
|
+
}
|
|
309
|
+
/** Options for {@link prismaAdapter}. */
|
|
310
|
+
interface PrismaAdapterOptions {
|
|
311
|
+
/**
|
|
312
|
+
* The default-deny allowlist. Only `(model, column)` pairs listed here are
|
|
313
|
+
* eligible for capture; an empty or unset list captures nothing. The
|
|
314
|
+
* server-side per-tenant allowlist re-enforces this independently at
|
|
315
|
+
* ingestion.
|
|
316
|
+
*/
|
|
317
|
+
allow?: ReadonlyArray<PrismaCaptureColumn>;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Build a passive Prisma value-capture extension. See the module doc for
|
|
321
|
+
* the full behavior contract.
|
|
322
|
+
*/
|
|
323
|
+
declare function prismaAdapter(options?: PrismaAdapterOptions): PrismaCaptureExtension;
|
|
324
|
+
|
|
160
325
|
/**
|
|
161
326
|
* Producer-sugar for computing boolean relations to emit as `*Holds`
|
|
162
327
|
* side-effect evidence.
|
|
@@ -216,4 +381,4 @@ declare function invariant<T extends number | string | bigint | boolean>(left: T
|
|
|
216
381
|
*/
|
|
217
382
|
declare function isNullInvariant(value: unknown): boolean;
|
|
218
383
|
|
|
219
|
-
export { type InvariantOp, type RecordSideEffectInput, SideEffectOperationKind, SideEffectOperationPhase, SideEffectOperationStatus, SideEffectSemanticFieldKey, invariant, isNullInvariant, recordSideEffect };
|
|
384
|
+
export { type CaptureOptions, type InvariantOp, type PrismaAdapterOptions, type PrismaCaptureColumn, type PrismaCaptureExtension, type RecordSideEffectInput, SideEffectOperationKind, SideEffectOperationPhase, SideEffectOperationStatus, SideEffectSemanticFieldKey, capture, invariant, isNullInvariant, prismaAdapter, recordSideEffect };
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GlasstraceExporter,
|
|
3
3
|
SessionManager,
|
|
4
|
+
attachScalar,
|
|
4
5
|
captureError,
|
|
6
|
+
checkScalarField,
|
|
5
7
|
classifyFetchTarget,
|
|
6
8
|
createGlasstraceSpanProcessor,
|
|
7
9
|
getDateString,
|
|
8
10
|
getDiscoveryHandler,
|
|
9
11
|
getOrigin,
|
|
12
|
+
recordOmission,
|
|
10
13
|
recordSideEffect,
|
|
11
14
|
registerGlasstrace,
|
|
15
|
+
reserveScalarSlot,
|
|
12
16
|
withGlasstraceConfig
|
|
13
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-Z2DSC3YI.js";
|
|
14
18
|
import {
|
|
15
19
|
getStatus,
|
|
16
20
|
isReady,
|
|
@@ -22,16 +26,20 @@ import {
|
|
|
22
26
|
captureCorrelationId
|
|
23
27
|
} from "./chunk-EVX6D2TX.js";
|
|
24
28
|
import "./chunk-CL3OVHPO.js";
|
|
25
|
-
import
|
|
29
|
+
import {
|
|
30
|
+
SpanKind,
|
|
31
|
+
trace
|
|
32
|
+
} from "./chunk-DQ25VOKK.js";
|
|
26
33
|
import "./chunk-YG3X7TUI.js";
|
|
27
34
|
import {
|
|
28
35
|
getActiveConfig,
|
|
29
36
|
getLinkedAccountId,
|
|
37
|
+
isCaptureEnabled,
|
|
30
38
|
loadCachedConfig,
|
|
31
39
|
performInit,
|
|
32
40
|
saveCachedConfig,
|
|
33
41
|
sendInitRequest
|
|
34
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-SONZOTBP.js";
|
|
35
43
|
import {
|
|
36
44
|
isAnonymousMode,
|
|
37
45
|
isProductionDisabled,
|
|
@@ -49,6 +57,116 @@ import {
|
|
|
49
57
|
import "./chunk-YIEXKQYP.js";
|
|
50
58
|
import "./chunk-NSBPE2FW.js";
|
|
51
59
|
|
|
60
|
+
// src/side-effect/capture.ts
|
|
61
|
+
function capture(name, value, options) {
|
|
62
|
+
try {
|
|
63
|
+
runCapture(name, value, options);
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function runCapture(name, value, options) {
|
|
68
|
+
const span = options?.span;
|
|
69
|
+
if (!span) return;
|
|
70
|
+
if (!isCaptureEnabled()) return;
|
|
71
|
+
try {
|
|
72
|
+
if (typeof span.isRecording === "function" && !span.isRecording()) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const outcome = checkScalarField(name, value);
|
|
79
|
+
if (!outcome.accepted) {
|
|
80
|
+
recordOmission(span, outcome.reason);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!reserveScalarSlot(span)) {
|
|
84
|
+
recordOmission(span, "value_too_long");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
attachScalar(span, name, outcome.value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/adapters/prisma.ts
|
|
91
|
+
var TRACER_NAME = "glasstrace-prisma";
|
|
92
|
+
function hasRecordingActiveSpan() {
|
|
93
|
+
try {
|
|
94
|
+
const span = trace.getActiveSpan();
|
|
95
|
+
if (span === void 0) return false;
|
|
96
|
+
if (typeof span.isRecording === "function" && !span.isRecording()) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function openOwnedSpan(model, operation) {
|
|
105
|
+
try {
|
|
106
|
+
return trace.getTracer(TRACER_NAME).startSpan(`db.${model}.${operation}`, { kind: SpanKind.CLIENT });
|
|
107
|
+
} catch {
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function deriveFlagKey(column) {
|
|
112
|
+
return column.endsWith("Flag") ? column : `${column}Flag`;
|
|
113
|
+
}
|
|
114
|
+
function projectAllowlisted(span, columns, result) {
|
|
115
|
+
if (result === null || typeof result !== "object" || Array.isArray(result)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const row = result;
|
|
119
|
+
for (const column of columns) {
|
|
120
|
+
if (!(column in row)) continue;
|
|
121
|
+
capture(deriveFlagKey(column), row[column], { span });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function prismaAdapter(options = {}) {
|
|
125
|
+
const policy = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const entry of options?.allow ?? []) {
|
|
127
|
+
if (!entry || typeof entry.model !== "string" || typeof entry.column !== "string" || entry.model.length === 0 || entry.column.length === 0) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
let columns = policy.get(entry.model);
|
|
131
|
+
if (!columns) {
|
|
132
|
+
columns = /* @__PURE__ */ new Set();
|
|
133
|
+
policy.set(entry.model, columns);
|
|
134
|
+
}
|
|
135
|
+
columns.add(entry.column);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
name: "glasstrace-capture",
|
|
139
|
+
query: {
|
|
140
|
+
$allModels: {
|
|
141
|
+
async $allOperations(params) {
|
|
142
|
+
const { model, operation, args, query } = params;
|
|
143
|
+
const columns = model !== void 0 ? policy.get(model) : void 0;
|
|
144
|
+
if (model === void 0 || columns === void 0 || operation === "findMany" || !isCaptureEnabled() || !hasRecordingActiveSpan()) {
|
|
145
|
+
return query(args);
|
|
146
|
+
}
|
|
147
|
+
const span = openOwnedSpan(model, operation);
|
|
148
|
+
if (span === void 0) {
|
|
149
|
+
return query(args);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const result = await query(args);
|
|
153
|
+
try {
|
|
154
|
+
projectAllowlisted(span, columns, result);
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
} finally {
|
|
159
|
+
try {
|
|
160
|
+
span.end();
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
52
170
|
// src/side-effect/invariant.ts
|
|
53
171
|
function invariant(left, op, right) {
|
|
54
172
|
switch (op) {
|
|
@@ -76,6 +194,7 @@ export {
|
|
|
76
194
|
GlasstraceSpanProcessor,
|
|
77
195
|
SdkError,
|
|
78
196
|
SessionManager,
|
|
197
|
+
capture,
|
|
79
198
|
captureCorrelationId,
|
|
80
199
|
captureError,
|
|
81
200
|
classifyFetchTarget,
|
|
@@ -96,6 +215,7 @@ export {
|
|
|
96
215
|
isSideEffectSemanticFieldKey,
|
|
97
216
|
loadCachedConfig,
|
|
98
217
|
performInit,
|
|
218
|
+
prismaAdapter,
|
|
99
219
|
readAnonKey,
|
|
100
220
|
readEnvVars,
|
|
101
221
|
recordSideEffect,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/side-effect/invariant.ts"],"sourcesContent":["/**\n * Producer-sugar for computing boolean relations to emit as `*Holds`\n * side-effect evidence.\n *\n * These helpers turn a comparison into the boolean a producer passes to\n * `recordSideEffect({ relations: { …Holds: invariant(a, \"eq\", b) } })`.\n * They are pure (no I/O, no Node built-ins) and edge-safe, so they live\n * on the root barrel. The operator set is intentionally minimal and\n * fixed — six binary comparisons plus a separate unary null check — and\n * is not a general expression DSL.\n */\n\n/**\n * The six supported binary comparison operators. `isNull` is **not** an\n * operator here — use {@link isNullInvariant} for the unary case.\n */\nexport type InvariantOp = \"eq\" | \"neq\" | \"lt\" | \"lte\" | \"gt\" | \"gte\";\n\n/**\n * Evaluate a binary comparison invariant and return the boolean result.\n *\n * Both operands are constrained to the same primitive type. `eq`/`neq`\n * use strict equality; the ordering operators (`lt`/`lte`/`gt`/`gte`)\n * use the language relational operators (numeric for numbers/bigints,\n * lexical for strings). Intended for producing a `*Holds` relation, e.g.\n * `invariant(emittedDurationMinutes, \"eq\", declaredDurationMinutes)`.\n *\n * Operands should be comparable primitives. `NaN` follows IEEE-754\n * (unequal to everything; all orderings `false`), so screen `NaN` before\n * asserting a relation. Passing a non-primitive (e.g. a `Symbol`, or an\n * object with a throwing `valueOf`) to an ordering operator throws per\n * JS semantics — the type signature prevents this for typed callers.\n *\n * @param left - The left operand.\n * @param op - One of the six {@link InvariantOp} comparisons.\n * @param right - The right operand (same primitive type as `left`).\n * @returns The boolean result of `left <op> right`.\n *\n * @example\n * recordSideEffect({\n * kind: \"calendar_link\",\n * operation: \"invite.create\",\n * relations: {\n * durationMatchesHolds: invariant(emittedMinutes, \"eq\", declaredMinutes),\n * },\n * });\n */\nexport function invariant<T extends number | string | bigint | boolean>(\n left: T,\n op: InvariantOp,\n right: T,\n): boolean {\n switch (op) {\n case \"eq\":\n return left === right;\n case \"neq\":\n return left !== right;\n case \"lt\":\n return left < right;\n case \"lte\":\n return left <= right;\n case \"gt\":\n return left > right;\n case \"gte\":\n return left >= right;\n }\n // For well-typed callers `op` is `never` here, so this `satisfies`\n // enforces switch exhaustiveness at compile time (adding an\n // `InvariantOp` member without a case is a type error). The `return`\n // is the runtime fallback for an untyped (JS) caller passing an\n // out-of-domain op — it yields a `boolean`, never `undefined`.\n op satisfies never;\n return false;\n}\n\n/**\n * Unary null/undefined invariant — `true` when `value` is `null` or\n * `undefined`. Kept separate from {@link invariant} because nullishness\n * is a unary predicate, not a binary comparison (there is no `isNull`\n * operator). Use for a `*Holds` relation asserting a value's absence,\n * e.g. `relations: { recipientMissingHolds: isNullInvariant(recipient) }`.\n *\n * @param value - The value to test.\n * @returns `true` when `value` is `null` or `undefined`, else `false`\n * (falsy-but-present values like `0`, `\"\"`, `false`, `NaN` are `false`).\n */\nexport function isNullInvariant(value: unknown): boolean {\n return value === null || value === undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CO,SAAS,UACd,MACA,IACA,OACS;AACT,UAAQ,IAAI;AAAA,IACV,KAAK;AACH,aAAO,SAAS;AAAA,IAClB,KAAK;AACH,aAAO,SAAS;AAAA,IAClB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,QAAQ;AAAA,IACjB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,QAAQ;AAAA,EACnB;AAMA;AACA,SAAO;AACT;AAaO,SAAS,gBAAgB,OAAyB;AACvD,SAAO,UAAU,QAAQ,UAAU;AACrC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/side-effect/capture.ts","../src/adapters/prisma.ts","../src/side-effect/invariant.ts"],"sourcesContent":["/**\n * Public value-capture primitive (L1 passive capture).\n *\n * {@link capture} emits a single allowlisted value-fidelity scalar onto a\n * caller-**owned** OTel span. It is the counterpart to {@link\n * recordSideEffect} for the case where the emitter owns the target span\n * itself (e.g. a passive database adapter that opens a `db.<Model>.<op>`\n * span) rather than attaching to the ambient active span.\n *\n * Why this exists: {@link recordSideEffect} is ambient-only — it resolves\n * the active span via `getActiveSpan()`. A passive adapter cannot use it,\n * because at its capture point the ambient span is the database client's\n * own operation span, which has already ended and is non-recording. So the\n * adapter must own a fresh recording span and emit onto it explicitly.\n *\n * The behavior contract mirrors {@link recordSideEffect}: observational\n * only. `capture` never executes a side effect, never reads or mutates the\n * captured value's source, and **never throws**. Every failure mode\n * (capture-config disabled, ended / `NonRecordingSpan` target, allowlist\n * rejection, OTel attribute-slot exhaustion) routes to a silent no-op or to\n * an omission-counter increment that carries no rejected input.\n *\n * Capture is **strict-mode only**: timestamp-shaped and unhashed-identifier\n * values are rejected at emit so they never reach the wire. The `full`\n * fidelity relaxation is not reachable through this primitive.\n */\n\nimport { type Span } from \"@opentelemetry/api\";\nimport { checkScalarField } from \"./allowlist.js\";\nimport { attachScalar, recordOmission, reserveScalarSlot } from \"./emit.js\";\nimport { isCaptureEnabled } from \"../init-client.js\";\n\n/** Options for {@link capture}. */\nexport interface CaptureOptions {\n /**\n * The caller-owned, recording span to attach the scalar to. `capture`\n * writes only to this span; it never resolves or touches the ambient\n * active span. The caller is responsible for `end()`-ing it.\n */\n span: Span;\n}\n\n/**\n * Emit a single allowlisted value-fidelity scalar onto a caller-owned span.\n *\n * The scalar `name` must match the value-fidelity scalar key pattern\n * (`*Ms` / `*Amount` / `*Bytes` / `*Ratio` / `*Id` / `*Value` / `*Flag`)\n * and its value must match the suffix type — e.g. a `*Flag` key requires a\n * `boolean`. Mismatches, timestamp-shaped values, and unhashed `*Id`s are\n * rejected under strict mode and recorded as an omission count on the\n * supplied span (never the active span).\n *\n * Edge cases (all silent no-ops, never throws):\n * - capture-config flag `sideEffectEvidence` is `false` ⇒ no-op, **no\n * counter** (mirrors {@link recordSideEffect}: with capture disabled the\n * SDK does no allowlist evaluation and writes nothing).\n * - the supplied span has already ended or is a `NonRecordingSpan` ⇒\n * no-op, **no counter** (its omission counter would itself be a dropped\n * span write). The owning adapter passes a fresh recording span, so this\n * is a caller-misuse guard.\n * - the value fails strict allowlist validation ⇒ an omission count is\n * recorded on the supplied span; the rejected value is never emitted.\n * - OTel attribute-slot exhaustion ⇒ the attribute write is silently\n * dropped.\n *\n * @example Project a boolean result field onto an owned database span\n * ```ts\n * import { capture } from \"@glasstrace/sdk\";\n *\n * const span = tracer.startSpan(\"db.Poll.findUnique\");\n * try {\n * const row = await query(args);\n * if (row) capture(\"mutedFlag\", row.muted, { span });\n * return row;\n * } finally {\n * span.end();\n * }\n * ```\n */\nexport function capture(\n name: string,\n value: unknown,\n options: CaptureOptions,\n): void {\n try {\n runCapture(name, value, options);\n } catch {\n // Defense-in-depth: an unexpected throw (e.g. a host shim\n // mis-implementing the OTel API) must never propagate to the\n // caller's request path. Capture is observationally invisible.\n }\n}\n\nfunction runCapture(\n name: string,\n value: unknown,\n options: CaptureOptions,\n): void {\n const span = options?.span;\n if (!span) return;\n\n // Capture-config gate first: read at every call so config rotation\n // takes effect on the next emission. With the flag off the SDK does\n // nothing and records no counter (a counter would itself require a\n // span write); this is the maximally fail-closed default.\n if (!isCaptureEnabled()) return;\n\n // Caller-misuse guard: an ended / NonRecordingSpan cannot carry the\n // scalar, and its omission counter would itself be a dropped write —\n // no-op entirely (no counter). The owning adapter passes a fresh\n // recording span, so correct use never reaches this branch.\n try {\n if (typeof span.isRecording === \"function\" && !span.isRecording()) {\n return;\n }\n } catch {\n return;\n }\n\n // Strict scalar validation — the only mode this primitive supports.\n const outcome = checkScalarField(name, value);\n if (!outcome.accepted) {\n // The omission count lands on the caller-supplied span — never the\n // ambient active span (which `capture` deliberately never resolves).\n recordOmission(span, outcome.reason);\n return;\n }\n\n // Enforce the per-operation scalar budget across many `capture()` calls on\n // the same owned span (e.g. an adapter projecting several columns). Beyond\n // the budget, deterministically omit rather than over-emit for downstream\n // truncation — mirroring `recordSideEffect`'s budget handling.\n if (!reserveScalarSlot(span)) {\n recordOmission(span, \"value_too_long\");\n return;\n }\n\n attachScalar(span, name, outcome.value);\n}\n","/**\n * Passive Prisma value-capture adapter (L1 capture).\n *\n * `prismaAdapter({ allow })` returns a Prisma client extension that, for\n * each allowlisted `(model, column)`, projects a boolean result field onto\n * a Glasstrace value-fidelity scalar so an agent can read it back from the\n * trace. It is **passive and observational**: it never executes a query\n * itself, never reads or mutates the result, and never changes query\n * behavior or errors.\n *\n * Apply it like any Prisma extension:\n *\n * ```ts\n * import { prismaAdapter } from \"@glasstrace/sdk\";\n *\n * const prisma = new PrismaClient().$extends(\n * prismaAdapter({ allow: [{ model: \"Poll\", column: \"muted\" }] }),\n * );\n * ```\n *\n * Design:\n * - **OWN a span.** At the capture point the database client's own\n * operation span has already ended, so the adapter opens its own\n * recording `db.<Model>.<op>` span (parented under the active request\n * span) and emits onto it via {@link capture}.\n * - **Default-deny.** Nothing is captured unless an explicit `allow` entry\n * matches AND the server-pushed `sideEffectEvidence` capture flag is on.\n * An empty / unset `allow` captures nothing.\n * - **Boolean only.** This adapter projects boolean columns (the strict,\n * no-`captureFidelity:full` case). Non-boolean allowlisted columns route\n * to a safe omission counter, never a captured value.\n * - **Pure observer.** Capture work can never throw into the host query;\n * the owned span is always ended; the original query error is re-thrown\n * verbatim.\n * - **Bounded.** `findMany` / list operations are disabled (no per-row\n * capture). The adapter never widens the app's `select`.\n *\n * This module has **no dependency on `@prisma/client`** — it is typed\n * structurally against Prisma's client-extension shape (mirroring the\n * Drizzle adapter), so it adds no runtime dependency and ships on the edge-\n * safe root barrel. On a runtime with no active request span (e.g. an edge\n * runtime with no AsyncLocalStorage), it captures nothing.\n */\n\nimport { trace, SpanKind, type Span } from \"@opentelemetry/api\";\nimport { capture } from \"../side-effect/capture.js\";\nimport { isCaptureEnabled } from \"../init-client.js\";\n\n/** The arguments Prisma passes to a `$allOperations` query-extension callback. */\ninterface PrismaAllOperationsArgs {\n /** The Prisma model name (PascalCase, e.g. `Poll`), or `undefined` for raw ops. */\n model?: string;\n /** The Prisma operation (e.g. `findUnique`, `findMany`, `update`). */\n operation: string;\n /** The operation arguments, forwarded unchanged to `query`. */\n args: unknown;\n /** Executes the underlying operation. Called exactly once. */\n query: (args: unknown) => Promise<unknown>;\n}\n\n/**\n * A Prisma client extension — the object passed to `prisma.$extends(...)`.\n * Structurally typed so the adapter needs no `@prisma/client` dependency.\n */\nexport interface PrismaCaptureExtension {\n name: string;\n query: {\n $allModels: {\n $allOperations(args: PrismaAllOperationsArgs): Promise<unknown>;\n };\n };\n}\n\n/** A single allowlisted column to project. */\nexport interface PrismaCaptureColumn {\n /** The Prisma model name, PascalCase, exactly as Prisma reports it (e.g. `Poll`). */\n model: string;\n /** The boolean result column to project (e.g. `muted`). */\n column: string;\n}\n\n/** Options for {@link prismaAdapter}. */\nexport interface PrismaAdapterOptions {\n /**\n * The default-deny allowlist. Only `(model, column)` pairs listed here are\n * eligible for capture; an empty or unset list captures nothing. The\n * server-side per-tenant allowlist re-enforces this independently at\n * ingestion.\n */\n allow?: ReadonlyArray<PrismaCaptureColumn>;\n}\n\nconst TRACER_NAME = \"glasstrace-prisma\";\n\n/**\n * Whether a **recording** request span is active, fail-closed. The adapter\n * parents its owned span under the request span and must capture nothing when\n * none is present (out-of-request / edge runtimes with no AsyncLocalStorage)\n * or when the active span is ended / a `NonRecordingSpan` (e.g. sampled out) —\n * mirroring `getRecordingActiveSpan` in `side-effect/emit.ts`. Wrapped so an\n * OTel API surface error can never propagate into the host query\n * (pure-observer).\n */\nfunction hasRecordingActiveSpan(): boolean {\n try {\n const span = trace.getActiveSpan();\n if (span === undefined) return false;\n // `isRecording()` is false for both NonRecordingSpan and ended spans; a\n // missing impl (host shim) is treated as recording, as elsewhere.\n if (typeof span.isRecording === \"function\" && !span.isRecording()) {\n return false;\n }\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Open the owned `db.<Model>.<op>` recording span, or `undefined` if the OTel\n * API throws. A `undefined` return makes the caller fall back to running the\n * query untouched — the capture machinery must never throw into the host\n * query (pure-observer).\n */\nfunction openOwnedSpan(model: string, operation: string): Span | undefined {\n try {\n return trace\n .getTracer(TRACER_NAME)\n .startSpan(`db.${model}.${operation}`, { kind: SpanKind.CLIENT });\n } catch {\n return undefined;\n }\n}\n\n/**\n * Derive the scalar key for a boolean column. A boolean projects onto a\n * `*Flag` scalar; the key is the column with a `Flag` suffix (not doubled if\n * the column already ends in `Flag`). This derivation is deterministic and\n * stable because the server-side operator allowlist keys on the emitted\n * scalar key (`<column>Flag`), not the source column.\n */\nfunction deriveFlagKey(column: string): string {\n return column.endsWith(\"Flag\") ? column : `${column}Flag`;\n}\n\n/**\n * Project every allowlisted column present in a single-row result onto the\n * owned span via {@link capture}. Guards non-object results (a `findUnique`\n * miss returns `null`; aggregates return non-objects; lists are arrays).\n * Never throws — {@link capture} swallows its own errors, and the call is\n * additionally fenced so a malformed result can never affect the query.\n */\nfunction projectAllowlisted(\n span: Span,\n columns: ReadonlySet<string>,\n result: unknown,\n): void {\n if (result === null || typeof result !== \"object\" || Array.isArray(result)) {\n return;\n }\n const row = result as Record<string, unknown>;\n for (const column of columns) {\n if (!(column in row)) continue;\n capture(deriveFlagKey(column), row[column], { span });\n }\n}\n\n/**\n * Build a passive Prisma value-capture extension. See the module doc for\n * the full behavior contract.\n */\nexport function prismaAdapter(\n options: PrismaAdapterOptions = {},\n): PrismaCaptureExtension {\n // Compile the allowlist into model -> set(columns) once at construction.\n const policy = new Map<string, Set<string>>();\n for (const entry of options?.allow ?? []) {\n if (\n !entry ||\n typeof entry.model !== \"string\" ||\n typeof entry.column !== \"string\" ||\n entry.model.length === 0 ||\n entry.column.length === 0\n ) {\n continue;\n }\n let columns = policy.get(entry.model);\n if (!columns) {\n columns = new Set();\n policy.set(entry.model, columns);\n }\n columns.add(entry.column);\n }\n\n return {\n name: \"glasstrace-capture\",\n query: {\n $allModels: {\n async $allOperations(\n params: PrismaAllOperationsArgs,\n ): Promise<unknown> {\n const { model, operation, args, query } = params;\n\n // Decide eligibility BEFORE opening a span so the default-deny /\n // disabled path adds zero span volume (hot-path) and never emits\n // on an orphan (edge / no request context). All four gates:\n // - the model has an allow entry (default-deny);\n // - the operation is not a multi-row list op (list/`findMany`\n // capture is disabled until a per-row cap + selection rule is\n // specified);\n // - the capture master switch is on (fail-closed default off);\n // - a recording request span is active (in-request, same-trace;\n // edge has no ALS / no active span, and a sampled-out span is\n // non-recording — capture nothing in both cases).\n const columns =\n model !== undefined ? policy.get(model) : undefined;\n if (\n model === undefined ||\n columns === undefined ||\n operation === \"findMany\" ||\n !isCaptureEnabled() ||\n !hasRecordingActiveSpan()\n ) {\n return query(args);\n }\n\n // OWN a recording db.<Model>.<op> span, parented under the active\n // request span. The span name is the attribution anchor. If the\n // OTel API fails to open the span, fall back to running the query\n // untouched — the capture path must never throw into it.\n const span = openOwnedSpan(model, operation);\n if (span === undefined) {\n return query(args);\n }\n try {\n const result = await query(args);\n // Fence projection so a malformed result can never alter the\n // query's own outcome (pure-observer invariant).\n try {\n projectAllowlisted(span, columns, result);\n } catch {\n // Never let capture work affect the host query result.\n }\n return result;\n } finally {\n // Always end the owned span, even when `query` throws; the\n // original error propagates verbatim (not swallowed). The end()\n // is itself guarded so it cannot mask that error.\n try {\n span.end();\n } catch {\n // OTel end() failure must not surface to the host query.\n }\n }\n },\n },\n },\n };\n}\n","/**\n * Producer-sugar for computing boolean relations to emit as `*Holds`\n * side-effect evidence.\n *\n * These helpers turn a comparison into the boolean a producer passes to\n * `recordSideEffect({ relations: { …Holds: invariant(a, \"eq\", b) } })`.\n * They are pure (no I/O, no Node built-ins) and edge-safe, so they live\n * on the root barrel. The operator set is intentionally minimal and\n * fixed — six binary comparisons plus a separate unary null check — and\n * is not a general expression DSL.\n */\n\n/**\n * The six supported binary comparison operators. `isNull` is **not** an\n * operator here — use {@link isNullInvariant} for the unary case.\n */\nexport type InvariantOp = \"eq\" | \"neq\" | \"lt\" | \"lte\" | \"gt\" | \"gte\";\n\n/**\n * Evaluate a binary comparison invariant and return the boolean result.\n *\n * Both operands are constrained to the same primitive type. `eq`/`neq`\n * use strict equality; the ordering operators (`lt`/`lte`/`gt`/`gte`)\n * use the language relational operators (numeric for numbers/bigints,\n * lexical for strings). Intended for producing a `*Holds` relation, e.g.\n * `invariant(emittedDurationMinutes, \"eq\", declaredDurationMinutes)`.\n *\n * Operands should be comparable primitives. `NaN` follows IEEE-754\n * (unequal to everything; all orderings `false`), so screen `NaN` before\n * asserting a relation. Passing a non-primitive (e.g. a `Symbol`, or an\n * object with a throwing `valueOf`) to an ordering operator throws per\n * JS semantics — the type signature prevents this for typed callers.\n *\n * @param left - The left operand.\n * @param op - One of the six {@link InvariantOp} comparisons.\n * @param right - The right operand (same primitive type as `left`).\n * @returns The boolean result of `left <op> right`.\n *\n * @example\n * recordSideEffect({\n * kind: \"calendar_link\",\n * operation: \"invite.create\",\n * relations: {\n * durationMatchesHolds: invariant(emittedMinutes, \"eq\", declaredMinutes),\n * },\n * });\n */\nexport function invariant<T extends number | string | bigint | boolean>(\n left: T,\n op: InvariantOp,\n right: T,\n): boolean {\n switch (op) {\n case \"eq\":\n return left === right;\n case \"neq\":\n return left !== right;\n case \"lt\":\n return left < right;\n case \"lte\":\n return left <= right;\n case \"gt\":\n return left > right;\n case \"gte\":\n return left >= right;\n }\n // For well-typed callers `op` is `never` here, so this `satisfies`\n // enforces switch exhaustiveness at compile time (adding an\n // `InvariantOp` member without a case is a type error). The `return`\n // is the runtime fallback for an untyped (JS) caller passing an\n // out-of-domain op — it yields a `boolean`, never `undefined`.\n op satisfies never;\n return false;\n}\n\n/**\n * Unary null/undefined invariant — `true` when `value` is `null` or\n * `undefined`. Kept separate from {@link invariant} because nullishness\n * is a unary predicate, not a binary comparison (there is no `isNull`\n * operator). Use for a `*Holds` relation asserting a value's absence,\n * e.g. `relations: { recipientMissingHolds: isNullInvariant(recipient) }`.\n *\n * @param value - The value to test.\n * @returns `true` when `value` is `null` or `undefined`, else `false`\n * (falsy-but-present values like `0`, `\"\"`, `false`, `NaN` are `false`).\n */\nexport function isNullInvariant(value: unknown): boolean {\n return value === null || value === undefined;\n}\n"],"mappings":"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","names":[]}
|