@lunora/do 1.0.0-alpha.39 → 1.0.0-alpha.40

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/index.d.mts CHANGED
@@ -4758,6 +4758,24 @@ declare abstract class ShardDO {
4758
4758
  * fields.
4759
4759
  */
4760
4760
  private currentRequestTrace;
4761
+ /**
4762
+ * Per-trace head-sampling state, keyed by `traceId` so concurrent dispatches on
4763
+ * the same DO instance can't clobber each other's decision. A DO interleaves
4764
+ * dispatches across `await` points, and the span-recording and error-flush
4765
+ * choke points run after a span body (or the whole dispatch) settles — by which
4766
+ * point a flat per-instance field could have been overwritten by a sibling
4767
+ * dispatch (the same hazard `dispatchTrace` guards against for the trace anchor).
4768
+ * Each entry holds `sampled` (the inbound `traceparent` flag; absent means keep,
4769
+ * so alarms, subscription re-runs and non-Lunora callers export exactly as
4770
+ * before — when false, `ctx.trace` INTERNAL spans are held out of the live export
4771
+ * and re-decided in the dispatch `finally` as a tail bias), `keepErrors` (the
4772
+ * runtime's `x-lunora-sample-errors` / `alwaysSampleErrors` toggle — a sampled-out
4773
+ * trace that errored is still exported whole unless off), and `sink` (captured
4774
+ * when a sampled-out span is first held, so the `finally` can flush the trace's
4775
+ * held spans). Registered at dispatch entry by the dispatch's own `traceId`, read
4776
+ * by `span.traceId`, and deleted in the `finally`.
4777
+ */
4778
+ private traceSampling;
4761
4779
  /**
4762
4780
  * Client-issued idempotency key for the in-flight mutation, forwarded via the
4763
4781
  * `x-lunora-mutation-id` header. When set, the dispatch path dedups the call
@@ -5941,8 +5959,35 @@ declare abstract class ShardDO {
5941
5959
  * `sink.onSpan`. Best-effort throughout, exactly like {@link recordUserLog}:
5942
5960
  * a span is recorded *after* its body already settled, so letting a telemetry
5943
5961
  * failure escape here would turn a succeeded operation into a failed request.
5962
+ *
5963
+ * The span is ALWAYS buffered locally (the Studio Traces panel is a full
5964
+ * "recent traces on this instance" readout, unaffected by sampling). Only the
5965
+ * export to the sink is sampled: when the active dispatch's trace was sampled
5966
+ * OUT (its inbound `traceparent` flag was `00`), the span is held back rather
5967
+ * than streamed — the dispatch `finally` re-decides once the trace's error
5968
+ * status is known ({@link flushSampledOutTrace}), so an errored trace is still
5969
+ * exported whole (tail bias). Spans from a sampled-in dispatch, or from another
5970
+ * trace (a subscription re-run mints its own anchor), stream immediately.
5944
5971
  */
5945
5972
  protected recordSpan(span: SpanEvent, sink?: TelemetrySink): void;
5973
+ /**
5974
+ * Hand one span to `sink.onSpan`, swallowing sink throws. The DO's `waitUntil`
5975
+ * is threaded so a network sink (otlpSink) can keep its export alive past the
5976
+ * response, matching the log path. Extracted so both the live-stream path in
5977
+ * {@link recordSpan} and the deferred error-keep flush in
5978
+ * {@link flushSampledOutTrace} share one guarded emit.
5979
+ */
5980
+ private emitSpan;
5981
+ /**
5982
+ * Export-boundary decision for a sampled-out trace, run from the dispatch
5983
+ * `finally` once the error status is known. A sampled-in trace already
5984
+ * streamed live, so this returns early for it; a sampled-out trace exports its
5985
+ * held `ctx.trace` spans only when `alwaysSampleErrors` is set AND the trace
5986
+ * errored (the dispatch threw, or a held span settled `ok: false`) — the tail
5987
+ * bias — and otherwise drops them. Held spans are read back from the local ring
5988
+ * (excluding the synthetic dispatch root, which never goes to `onSpan`).
5989
+ */
5990
+ private flushSampledOutTrace;
5946
5991
  /**
5947
5992
  * Assemble the per-socket {@link LifecycleDispatchInfo} from its attachment:
5948
5993
  * the verified identity to replay and the {@link LifecycleEvent} the hooks
package/dist/index.d.ts CHANGED
@@ -4758,6 +4758,24 @@ declare abstract class ShardDO {
4758
4758
  * fields.
4759
4759
  */
4760
4760
  private currentRequestTrace;
4761
+ /**
4762
+ * Per-trace head-sampling state, keyed by `traceId` so concurrent dispatches on
4763
+ * the same DO instance can't clobber each other's decision. A DO interleaves
4764
+ * dispatches across `await` points, and the span-recording and error-flush
4765
+ * choke points run after a span body (or the whole dispatch) settles — by which
4766
+ * point a flat per-instance field could have been overwritten by a sibling
4767
+ * dispatch (the same hazard `dispatchTrace` guards against for the trace anchor).
4768
+ * Each entry holds `sampled` (the inbound `traceparent` flag; absent means keep,
4769
+ * so alarms, subscription re-runs and non-Lunora callers export exactly as
4770
+ * before — when false, `ctx.trace` INTERNAL spans are held out of the live export
4771
+ * and re-decided in the dispatch `finally` as a tail bias), `keepErrors` (the
4772
+ * runtime's `x-lunora-sample-errors` / `alwaysSampleErrors` toggle — a sampled-out
4773
+ * trace that errored is still exported whole unless off), and `sink` (captured
4774
+ * when a sampled-out span is first held, so the `finally` can flush the trace's
4775
+ * held spans). Registered at dispatch entry by the dispatch's own `traceId`, read
4776
+ * by `span.traceId`, and deleted in the `finally`.
4777
+ */
4778
+ private traceSampling;
4761
4779
  /**
4762
4780
  * Client-issued idempotency key for the in-flight mutation, forwarded via the
4763
4781
  * `x-lunora-mutation-id` header. When set, the dispatch path dedups the call
@@ -5941,8 +5959,35 @@ declare abstract class ShardDO {
5941
5959
  * `sink.onSpan`. Best-effort throughout, exactly like {@link recordUserLog}:
5942
5960
  * a span is recorded *after* its body already settled, so letting a telemetry
5943
5961
  * failure escape here would turn a succeeded operation into a failed request.
5962
+ *
5963
+ * The span is ALWAYS buffered locally (the Studio Traces panel is a full
5964
+ * "recent traces on this instance" readout, unaffected by sampling). Only the
5965
+ * export to the sink is sampled: when the active dispatch's trace was sampled
5966
+ * OUT (its inbound `traceparent` flag was `00`), the span is held back rather
5967
+ * than streamed — the dispatch `finally` re-decides once the trace's error
5968
+ * status is known ({@link flushSampledOutTrace}), so an errored trace is still
5969
+ * exported whole (tail bias). Spans from a sampled-in dispatch, or from another
5970
+ * trace (a subscription re-run mints its own anchor), stream immediately.
5944
5971
  */
5945
5972
  protected recordSpan(span: SpanEvent, sink?: TelemetrySink): void;
5973
+ /**
5974
+ * Hand one span to `sink.onSpan`, swallowing sink throws. The DO's `waitUntil`
5975
+ * is threaded so a network sink (otlpSink) can keep its export alive past the
5976
+ * response, matching the log path. Extracted so both the live-stream path in
5977
+ * {@link recordSpan} and the deferred error-keep flush in
5978
+ * {@link flushSampledOutTrace} share one guarded emit.
5979
+ */
5980
+ private emitSpan;
5981
+ /**
5982
+ * Export-boundary decision for a sampled-out trace, run from the dispatch
5983
+ * `finally` once the error status is known. A sampled-in trace already
5984
+ * streamed live, so this returns early for it; a sampled-out trace exports its
5985
+ * held `ctx.trace` spans only when `alwaysSampleErrors` is set AND the trace
5986
+ * errored (the dispatch threw, or a held span settled `ok: false`) — the tail
5987
+ * bias — and otherwise drops them. Held spans are read back from the local ring
5988
+ * (excluding the synthetic dispatch root, which never goes to `onSpan`).
5989
+ */
5990
+ private flushSampledOutTrace;
5946
5991
  /**
5947
5992
  * Assemble the per-socket {@link LifecycleDispatchInfo} from its attachment:
5948
5993
  * the verified identity to replay and the {@link LifecycleEvent} the hooks
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ export { AGGREGATE_SQL_FUNCTION, aggregateSqlFunction, matchesStaticWhere, norma
3
3
  export { aggregateTableName, coerceAggregateNumber, encodeAggregateKey, foldAggregateTally, readAggregateValue } from './packem_shared/aggregateTableName-CxNqY1Sl.mjs';
4
4
  export { CountRlsUnsupportedError, mergeWhere, planAggregateLookup, selectIndexForAggregate, selectIndexForCount, selectIndexForGroupBy } from './packem_shared/CountRlsUnsupportedError-BGxj0pgS.mjs';
5
5
  export { AUTH_METRICS_BUCKETS_TABLE, AUTH_METRICS_BUCKET_MS, AUTH_METRICS_BUCKET_RETENTION, AUTH_METRICS_TABLE, ensureAuthMetricsTables, readAuthMetrics, recordAuthEvent } from './packem_shared/AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
6
- export { c as createMetrics, a as createTracer, d as dispatchRootSpan } from './packem_shared/context-telemetry-CqcOObBl.mjs';
6
+ export { c as createMetrics, a as createTracer, d as dispatchRootSpan } from './packem_shared/context-telemetry-BVSDl6PU.mjs';
7
7
  export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStructurally } from './packem_shared/NotUniqueError-DbtlWwcG.mjs';
8
8
  export { DATA_MIGRATION_STATE_TABLE, readMigrationStatus, runDataMigration } from './packem_shared/DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs';
9
9
  export { SCAN_DEP, createDependencyTracker, depKey } from './packem_shared/SCAN_DEP-DLJF8dsj.mjs';
@@ -28,7 +28,7 @@ export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter } from './packem_share
28
28
  export { buildFtsMatch, ftsTableName, scoreDocument, stringifySearchText, tokenizeSearch } from './packem_shared/buildFtsMatch-BLEMawrp.mjs';
29
29
  export { M as MIN_ADMIN_TOKEN_LENGTH, a as MIN_AUTH_SECRET_LENGTH, b as buildSecurityAudit } from './packem_shared/security-audit-CucgBice.mjs';
30
30
  export { SESSION_DO_TTL_DEFAULT, SessionDO } from './packem_shared/SESSION_DO_TTL_DEFAULT-BnSKgVO4.mjs';
31
- export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-CJGwzfoT.mjs';
31
+ export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-COEnnBhO.mjs';
32
32
  export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-D99roc-r.mjs';
33
33
  export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-iFAA8FbD.mjs';
34
34
  export { createSystemReader } from './packem_shared/createSystemReader-D12eNH13.mjs';
@@ -2,10 +2,10 @@ import { LunoraError, toErrorBody } from '@lunora/errors';
2
2
  import { drizzle } from 'drizzle-orm/durable-sqlite';
3
3
  import { c as constantTimeEqual } from './constant-time-equal-BVRWZgES.mjs';
4
4
  import { j as jsonResponse } from './json-response-BdbtpOhm.mjs';
5
+ import { n as normalizeLogFields, r as resolveTraceAnchor, p as parseTraceparent, a as createTracer, c as createMetrics, d as dispatchRootSpan } from './context-telemetry-BVSDl6PU.mjs';
5
6
  import { e as encodeWire, d as decodeWire } from './wire-codec-CzQc1pvf.mjs';
6
7
  import { parseExportShardArgs, parseImportShardArgs } from './exportShardRows-Dy3oFZ26.mjs';
7
8
  import { recordAuthEvent, readAuthMetrics } from './AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
8
- import { n as normalizeLogFields, r as resolveTraceAnchor, a as createTracer, c as createMetrics, d as dispatchRootSpan } from './context-telemetry-CqcOObBl.mjs';
9
9
  import { DATA_MIGRATION_STATE_TABLE, readMigrationStatus } from './DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs';
10
10
  import { SCAN_DEP, createDependencyTracker, tableFromDepKey } from './SCAN_DEP-DLJF8dsj.mjs';
11
11
  import { readFunctionMetricsTotals, readFunctionMetricIndexHits, recordFunctionMetric, mergeScanAttribution, readFunctionMetrics, readFunctionMetricBuckets } from './FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
@@ -2678,6 +2678,24 @@ class ShardDO {
2678
2678
  * fields.
2679
2679
  */
2680
2680
  currentRequestTrace;
2681
+ /**
2682
+ * Per-trace head-sampling state, keyed by `traceId` so concurrent dispatches on
2683
+ * the same DO instance can't clobber each other's decision. A DO interleaves
2684
+ * dispatches across `await` points, and the span-recording and error-flush
2685
+ * choke points run after a span body (or the whole dispatch) settles — by which
2686
+ * point a flat per-instance field could have been overwritten by a sibling
2687
+ * dispatch (the same hazard `dispatchTrace` guards against for the trace anchor).
2688
+ * Each entry holds `sampled` (the inbound `traceparent` flag; absent means keep,
2689
+ * so alarms, subscription re-runs and non-Lunora callers export exactly as
2690
+ * before — when false, `ctx.trace` INTERNAL spans are held out of the live export
2691
+ * and re-decided in the dispatch `finally` as a tail bias), `keepErrors` (the
2692
+ * runtime's `x-lunora-sample-errors` / `alwaysSampleErrors` toggle — a sampled-out
2693
+ * trace that errored is still exported whole unless off), and `sink` (captured
2694
+ * when a sampled-out span is first held, so the `finally` can flush the trace's
2695
+ * held spans). Registered at dispatch entry by the dispatch's own `traceId`, read
2696
+ * by `span.traceId`, and deleted in the `finally`.
2697
+ */
2698
+ traceSampling = /* @__PURE__ */ new Map();
2681
2699
  /**
2682
2700
  * Client-issued idempotency key for the in-flight mutation, forwarded via the
2683
2701
  * `x-lunora-mutation-id` header. When set, the dispatch path dedups the call
@@ -3013,6 +3031,10 @@ class ShardDO {
3013
3031
  this.currentRequestTraceparent = request.headers.get("traceparent") ?? void 0;
3014
3032
  this.currentRequestTrace = resolveTraceAnchor(this.currentRequestTraceparent);
3015
3033
  const dispatchTrace = this.currentRequestTrace;
3034
+ this.traceSampling.set(dispatchTrace.traceId, {
3035
+ keepErrors: request.headers.get("x-lunora-sample-errors") !== "0",
3036
+ sampled: parseTraceparent(this.currentRequestTraceparent)?.sampled ?? true
3037
+ });
3016
3038
  this.currentRequestReadTables = void 0;
3017
3039
  this.currentRequestCacheHit = void 0;
3018
3040
  this.metrics.requests += 1;
@@ -3075,6 +3097,8 @@ class ShardDO {
3075
3097
  if (this.spans.hasTrace(dispatchTrace.traceId)) {
3076
3098
  this.recordDispatchRootSpan(payload.functionPath, dispatchStartedAt, dispatchError, dispatchTrace);
3077
3099
  }
3100
+ this.flushSampledOutTrace(dispatchTrace, dispatchError !== void 0);
3101
+ this.traceSampling.delete(dispatchTrace.traceId);
3078
3102
  this.currentRequestTrace = void 0;
3079
3103
  this.currentRequestBookmark = void 0;
3080
3104
  this.currentResponseBookmark = void 0;
@@ -4852,6 +4876,15 @@ class ShardDO {
4852
4876
  * `sink.onSpan`. Best-effort throughout, exactly like {@link recordUserLog}:
4853
4877
  * a span is recorded *after* its body already settled, so letting a telemetry
4854
4878
  * failure escape here would turn a succeeded operation into a failed request.
4879
+ *
4880
+ * The span is ALWAYS buffered locally (the Studio Traces panel is a full
4881
+ * "recent traces on this instance" readout, unaffected by sampling). Only the
4882
+ * export to the sink is sampled: when the active dispatch's trace was sampled
4883
+ * OUT (its inbound `traceparent` flag was `00`), the span is held back rather
4884
+ * than streamed — the dispatch `finally` re-decides once the trace's error
4885
+ * status is known ({@link flushSampledOutTrace}), so an errored trace is still
4886
+ * exported whole (tail bias). Spans from a sampled-in dispatch, or from another
4887
+ * trace (a subscription re-run mints its own anchor), stream immediately.
4855
4888
  */
4856
4889
  // eslint-disable-next-line @typescript-eslint/member-ordering -- kept in the span-recording cluster (next to recordDispatchRootSpan) for cohesion rather than hoisted above every private member
4857
4890
  recordSpan(span, sink) {
@@ -4859,11 +4892,57 @@ class ShardDO {
4859
4892
  this.spans.push(span);
4860
4893
  } catch {
4861
4894
  }
4862
- if (sink?.onSpan) {
4863
- try {
4864
- sink.onSpan(span, { waitUntil: this.state.waitUntil?.bind(this.state) });
4865
- } catch {
4866
- }
4895
+ if (!sink?.onSpan) {
4896
+ return;
4897
+ }
4898
+ const sampling = this.traceSampling.get(span.traceId);
4899
+ if (sampling && !sampling.sampled) {
4900
+ sampling.sink = sink;
4901
+ return;
4902
+ }
4903
+ this.emitSpan(span, sink);
4904
+ }
4905
+ /**
4906
+ * Hand one span to `sink.onSpan`, swallowing sink throws. The DO's `waitUntil`
4907
+ * is threaded so a network sink (otlpSink) can keep its export alive past the
4908
+ * response, matching the log path. Extracted so both the live-stream path in
4909
+ * {@link recordSpan} and the deferred error-keep flush in
4910
+ * {@link flushSampledOutTrace} share one guarded emit.
4911
+ */
4912
+ emitSpan(span, sink) {
4913
+ if (!sink.onSpan) {
4914
+ return;
4915
+ }
4916
+ try {
4917
+ sink.onSpan(span, { waitUntil: this.state.waitUntil?.bind(this.state) });
4918
+ } catch {
4919
+ }
4920
+ }
4921
+ /**
4922
+ * Export-boundary decision for a sampled-out trace, run from the dispatch
4923
+ * `finally` once the error status is known. A sampled-in trace already
4924
+ * streamed live, so this returns early for it; a sampled-out trace exports its
4925
+ * held `ctx.trace` spans only when `alwaysSampleErrors` is set AND the trace
4926
+ * errored (the dispatch threw, or a held span settled `ok: false`) — the tail
4927
+ * bias — and otherwise drops them. Held spans are read back from the local ring
4928
+ * (excluding the synthetic dispatch root, which never goes to `onSpan`).
4929
+ */
4930
+ flushSampledOutTrace(trace, dispatchFailed) {
4931
+ const sampling = this.traceSampling.get(trace.traceId);
4932
+ if (!sampling || sampling.sampled || !sampling.keepErrors) {
4933
+ return;
4934
+ }
4935
+ const { sink } = sampling;
4936
+ if (!sink?.onSpan) {
4937
+ return;
4938
+ }
4939
+ const held = this.spans.entries().filter((span) => span.traceId === trace.traceId && span.dispatch !== true);
4940
+ const traceHasError = dispatchFailed || held.some((span) => !span.ok);
4941
+ if (!traceHasError) {
4942
+ return;
4943
+ }
4944
+ for (const span of held) {
4945
+ this.emitSpan(span, sink);
4867
4946
  }
4868
4947
  }
4869
4948
  /**
@@ -49,7 +49,7 @@ const parseTraceparent = (header) => {
49
49
  version === "00" && parts.length !== 4 || traceId === void 0 || parentSpanId === void 0 || flags === void 0 || flags.length !== 2 || !HEX_ONLY.test(flags) || traceId.length !== 32 || parentSpanId.length !== 16 || !HEX_ONLY.test(traceId) || !HEX_ONLY.test(parentSpanId) || traceId === "00000000000000000000000000000000" || parentSpanId === "0000000000000000") {
50
50
  return void 0;
51
51
  }
52
- return { parentSpanId, traceId };
52
+ return { parentSpanId, sampled: (Number.parseInt(flags, 16) & 1) === 1, traceId };
53
53
  };
54
54
 
55
55
  const resolveTraceAnchor = (traceparent) => {
@@ -172,4 +172,4 @@ const dispatchRootSpan = (input) => {
172
172
  };
173
173
  };
174
174
 
175
- export { createTracer as a, createMetrics as c, dispatchRootSpan as d, normalizeLogFields as n, resolveTraceAnchor as r };
175
+ export { createTracer as a, createMetrics as c, dispatchRootSpan as d, normalizeLogFields as n, parseTraceparent as p, resolveTraceAnchor as r };
@@ -1 +1 @@
1
- export { c as createMetrics, a as createTracer, d as dispatchRootSpan } from './context-telemetry-CqcOObBl.mjs';
1
+ export { c as createMetrics, a as createTracer, d as dispatchRootSpan } from './context-telemetry-BVSDl6PU.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/do",
3
- "version": "1.0.0-alpha.39",
3
+ "version": "1.0.0-alpha.40",
4
4
  "description": "Lunora Durable Objects: ShardDO (SQLite, OCC, hibernated WebSocket subscriptions) and SessionDO",
5
5
  "keywords": [
6
6
  "cloudflare",