@qlever-llc/trellis 0.10.10 → 0.10.12

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.
Files changed (131) hide show
  1. package/esm/client.d.ts +2 -0
  2. package/esm/client.d.ts.map +1 -1
  3. package/esm/client.js +2 -0
  4. package/esm/client_connect.d.ts +3 -2
  5. package/esm/client_connect.d.ts.map +1 -1
  6. package/esm/client_connect.js +4 -1
  7. package/esm/errors/TrellisError.d.ts +3 -3
  8. package/esm/errors/TrellisError.js +3 -3
  9. package/esm/server/health.d.ts.map +1 -1
  10. package/esm/server/health.js +34 -3
  11. package/esm/server/internal_jobs/job-manager.d.ts.map +1 -1
  12. package/esm/server/internal_jobs/job-manager.js +32 -1
  13. package/esm/server/runtime.d.ts +3 -0
  14. package/esm/server/runtime.d.ts.map +1 -1
  15. package/esm/server/service.d.ts +15 -0
  16. package/esm/server/service.d.ts.map +1 -1
  17. package/esm/server/service.js +41 -3
  18. package/esm/server.d.ts.map +1 -1
  19. package/esm/server.js +99 -10
  20. package/esm/service/deno.d.ts +1 -1
  21. package/esm/service/deno.d.ts.map +1 -1
  22. package/esm/service/mod.d.ts +1 -1
  23. package/esm/service/mod.d.ts.map +1 -1
  24. package/esm/service/node.d.ts +1 -1
  25. package/esm/service/node.d.ts.map +1 -1
  26. package/esm/service/outbox_inbox.d.ts.map +1 -1
  27. package/esm/service/outbox_inbox.js +14 -0
  28. package/esm/telemetry/core.d.ts.map +1 -1
  29. package/esm/telemetry/core.js +1 -1
  30. package/esm/telemetry/env.d.ts.map +1 -1
  31. package/esm/telemetry/env.js +6 -1
  32. package/esm/telemetry/init.d.ts +3 -0
  33. package/esm/telemetry/init.d.ts.map +1 -0
  34. package/esm/telemetry/init.js +7 -0
  35. package/esm/telemetry/metrics.d.ts +34 -0
  36. package/esm/telemetry/metrics.d.ts.map +1 -0
  37. package/esm/telemetry/metrics.js +181 -0
  38. package/esm/telemetry/mod.d.ts +3 -0
  39. package/esm/telemetry/mod.d.ts.map +1 -1
  40. package/esm/telemetry/mod.js +2 -0
  41. package/esm/telemetry/runtime.d.ts +2 -0
  42. package/esm/telemetry/runtime.d.ts.map +1 -0
  43. package/esm/telemetry/runtime.js +134 -0
  44. package/esm/telemetry.d.ts +3 -0
  45. package/esm/telemetry.d.ts.map +1 -0
  46. package/esm/telemetry.js +2 -0
  47. package/esm/transfer.d.ts.map +1 -1
  48. package/esm/transfer.js +27 -16
  49. package/esm/trellis.d.ts +28 -4
  50. package/esm/trellis.d.ts.map +1 -1
  51. package/esm/trellis.js +575 -80
  52. package/package.json +7 -5
  53. package/script/client.d.ts +2 -0
  54. package/script/client.d.ts.map +1 -1
  55. package/script/client.js +2 -0
  56. package/script/client_connect.d.ts +3 -2
  57. package/script/client_connect.d.ts.map +1 -1
  58. package/script/client_connect.js +4 -1
  59. package/script/errors/TrellisError.d.ts +3 -3
  60. package/script/errors/TrellisError.js +3 -3
  61. package/script/server/health.d.ts.map +1 -1
  62. package/script/server/health.js +34 -3
  63. package/script/server/internal_jobs/job-manager.d.ts.map +1 -1
  64. package/script/server/internal_jobs/job-manager.js +32 -1
  65. package/script/server/runtime.d.ts +3 -0
  66. package/script/server/runtime.d.ts.map +1 -1
  67. package/script/server/service.d.ts +15 -0
  68. package/script/server/service.d.ts.map +1 -1
  69. package/script/server/service.js +40 -2
  70. package/script/server.d.ts.map +1 -1
  71. package/script/server.js +98 -9
  72. package/script/service/deno.d.ts +1 -1
  73. package/script/service/deno.d.ts.map +1 -1
  74. package/script/service/mod.d.ts +1 -1
  75. package/script/service/mod.d.ts.map +1 -1
  76. package/script/service/node.d.ts +1 -1
  77. package/script/service/node.d.ts.map +1 -1
  78. package/script/service/outbox_inbox.d.ts.map +1 -1
  79. package/script/service/outbox_inbox.js +14 -0
  80. package/script/telemetry/core.d.ts.map +1 -1
  81. package/script/telemetry/core.js +1 -1
  82. package/script/telemetry/env.d.ts.map +1 -1
  83. package/script/telemetry/env.js +6 -1
  84. package/script/telemetry/init.d.ts +3 -0
  85. package/script/telemetry/init.d.ts.map +1 -0
  86. package/script/telemetry/init.js +10 -0
  87. package/script/telemetry/metrics.d.ts +34 -0
  88. package/script/telemetry/metrics.d.ts.map +1 -0
  89. package/script/telemetry/metrics.js +186 -0
  90. package/script/telemetry/mod.d.ts +3 -0
  91. package/script/telemetry/mod.d.ts.map +1 -1
  92. package/script/telemetry/mod.js +7 -1
  93. package/script/telemetry/runtime.d.ts +2 -0
  94. package/script/telemetry/runtime.d.ts.map +1 -0
  95. package/script/telemetry/runtime.js +137 -0
  96. package/script/telemetry.d.ts +3 -0
  97. package/script/telemetry.d.ts.map +1 -0
  98. package/script/telemetry.js +18 -0
  99. package/script/transfer.d.ts.map +1 -1
  100. package/script/transfer.js +28 -17
  101. package/script/trellis.d.ts +28 -4
  102. package/script/trellis.d.ts.map +1 -1
  103. package/script/trellis.js +606 -110
  104. package/src/client.ts +4 -0
  105. package/src/client_connect.ts +11 -9
  106. package/src/errors/TrellisError.ts +4 -4
  107. package/src/server/health.ts +41 -3
  108. package/src/server/internal_jobs/job-manager.ts +35 -5
  109. package/src/server/runtime.ts +4 -0
  110. package/src/server/service.ts +75 -3
  111. package/src/server.ts +124 -14
  112. package/src/service/deno.ts +1 -0
  113. package/src/service/mod.ts +1 -0
  114. package/src/service/node.ts +1 -0
  115. package/src/service/outbox_inbox.ts +14 -0
  116. package/src/telemetry/core.ts +1 -1
  117. package/src/telemetry/env.ts +5 -1
  118. package/src/telemetry/init.ts +8 -0
  119. package/src/telemetry/metrics.ts +294 -0
  120. package/src/telemetry/mod.ts +7 -0
  121. package/src/telemetry/runtime.ts +218 -0
  122. package/src/telemetry.ts +2 -0
  123. package/src/transfer.ts +69 -30
  124. package/src/trellis.ts +652 -141
  125. package/esm/tracing.d.ts +0 -5
  126. package/esm/tracing.d.ts.map +0 -1
  127. package/esm/tracing.js +0 -8
  128. package/script/tracing.d.ts +0 -5
  129. package/script/tracing.d.ts.map +0 -1
  130. package/script/tracing.js +0 -27
  131. package/src/tracing.ts +0 -28
package/src/trellis.ts CHANGED
@@ -47,12 +47,14 @@ import {
47
47
  createNatsHeaderCarrier,
48
48
  extractTraceContext,
49
49
  injectTraceContext,
50
+ recordTrellisError,
50
51
  SpanStatusCode,
51
52
  startClientSpan,
52
53
  startServerSpan,
53
54
  trace,
55
+ type TrellisErrorMetricAttributes,
54
56
  withSpanAsync,
55
- } from "./tracing.js";
57
+ } from "./telemetry/mod.js";
56
58
  import { Type } from "typebox";
57
59
  import { AssertError, Pointer } from "typebox/value";
58
60
  import { ulid } from "ulid";
@@ -732,6 +734,65 @@ export function isResultLike(
732
734
  ): value is Result<unknown, BaseError> {
733
735
  return value instanceof Result;
734
736
  }
737
+
738
+ type SerializableRuntimeError = {
739
+ id?: string;
740
+ type: string;
741
+ message: string;
742
+ context?: Record<string, unknown>;
743
+ traceId?: string;
744
+ } & Record<string, unknown>;
745
+
746
+ export type HandlerErrorAnnotationContext = {
747
+ method?: string;
748
+ event?: string;
749
+ feed?: string;
750
+ operation?: string;
751
+ jobType?: string;
752
+ requestId?: string;
753
+ service?: string;
754
+ contractId?: string;
755
+ contractDigest?: string;
756
+ traceId?: string;
757
+ };
758
+
759
+ function compactHandlerErrorContext(
760
+ context: HandlerErrorAnnotationContext,
761
+ ): Record<string, unknown> {
762
+ return Object.fromEntries(
763
+ Object.entries(context).filter(([key, value]) =>
764
+ key !== "traceId" && value !== undefined
765
+ ),
766
+ );
767
+ }
768
+
769
+ function sanitizeHandlerErrorContext(error: BaseError): void {
770
+ delete error.getContext().subject;
771
+ }
772
+
773
+ export function annotateHandlerBoundaryError(
774
+ cause: unknown,
775
+ context: HandlerErrorAnnotationContext,
776
+ ): BaseError {
777
+ const error = cause instanceof BaseError && !(cause instanceof RemoteError)
778
+ ? cause
779
+ : new UnexpectedError({ cause });
780
+ sanitizeHandlerErrorContext(error);
781
+ error.withContext(compactHandlerErrorContext(context));
782
+ error.withTraceId(context.traceId);
783
+ return error;
784
+ }
785
+
786
+ function recordRuntimeError(
787
+ error: unknown,
788
+ attributes: TrellisErrorMetricAttributes,
789
+ ): void {
790
+ recordTrellisError(error, {
791
+ messagingSystem: "nats",
792
+ ...attributes,
793
+ });
794
+ }
795
+
735
796
  export type RuntimeOperationDesc = {
736
797
  subject: string;
737
798
  input: unknown;
@@ -788,10 +849,7 @@ export type RuntimeOperationSnapshot = {
788
849
  progress?: unknown;
789
850
  transfer?: RuntimeOperationTransferProgress;
790
851
  output?: unknown;
791
- error?: {
792
- type: string;
793
- message: string;
794
- };
852
+ error?: SerializableRuntimeError;
795
853
  };
796
854
 
797
855
  export type RuntimeOperationRecord = {
@@ -848,8 +906,11 @@ const DurableOperationSnapshotSchema = Type.Object({
848
906
  })),
849
907
  output: Type.Optional(Type.Any()),
850
908
  error: Type.Optional(Type.Object({
909
+ id: Type.Optional(Type.String()),
851
910
  type: Type.String(),
852
911
  message: Type.String(),
912
+ context: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
913
+ traceId: Type.Optional(Type.String()),
853
914
  })),
854
915
  });
855
916
 
@@ -990,6 +1051,8 @@ export type TrellisOpts<TA extends AnyTrellisAPI> = {
990
1051
  state?: RuntimeStateStores;
991
1052
  connection?: TrellisConnection;
992
1053
  onSessionNotFound?: () => MaybePromise<void>;
1054
+ contractId?: string;
1055
+ contractDigest?: string;
993
1056
  };
994
1057
 
995
1058
  export type RequestOpts = {
@@ -1960,6 +2023,8 @@ export class Trellis<
1960
2023
  readonly handle: { readonly rpc: ActiveRpcHandleFacade<TA, TRequests> };
1961
2024
  /** Framework-neutral lifecycle handle for this Trellis runtime connection. */
1962
2025
  readonly connection: TrellisConnection;
2026
+ readonly contractId?: string;
2027
+ readonly contractDigest?: string;
1963
2028
 
1964
2029
  protected nats: NatsConnection;
1965
2030
  protected js: JetStreamClient;
@@ -1991,6 +2056,8 @@ export class Trellis<
1991
2056
  this.#log = (opts?.log ?? logger).child({ lib: "trellis" });
1992
2057
  this.timeout = opts?.timeout ?? 3000;
1993
2058
  this.stream = opts?.stream ?? "trellis";
2059
+ this.contractId = opts?.contractId;
2060
+ this.contractDigest = opts?.contractDigest;
1994
2061
  this.#hasExplicitApi = api !== undefined;
1995
2062
  this.#noResponderMaxRetries = opts?.noResponderRetry?.maxAttempts ??
1996
2063
  DEFAULT_NO_RESPONDER_MAX_RETRIES;
@@ -2409,11 +2476,23 @@ export class Trellis<
2409
2476
 
2410
2477
  const msg = encodeRuntimeSchema(ctx.input, input).take();
2411
2478
  if (isErr(msg)) {
2479
+ recordRuntimeError(msg.error, {
2480
+ surface: "rpc",
2481
+ direction: "client",
2482
+ operation: method,
2483
+ phase: "request_encoding",
2484
+ });
2412
2485
  return msg;
2413
2486
  }
2414
2487
 
2415
2488
  const subject = this.template(ctx.subject, input).take();
2416
2489
  if (isErr(subject)) {
2490
+ recordRuntimeError(subject.error, {
2491
+ surface: "rpc",
2492
+ direction: "client",
2493
+ operation: method,
2494
+ phase: "request_encoding",
2495
+ });
2417
2496
  return subject;
2418
2497
  }
2419
2498
 
@@ -2438,13 +2517,19 @@ export class Trellis<
2438
2517
  });
2439
2518
  const response = msgResult.take();
2440
2519
  if (isErr(response)) {
2520
+ recordRuntimeError(response.error, {
2521
+ surface: "rpc",
2522
+ direction: "client",
2523
+ operation: method,
2524
+ phase: "request_send",
2525
+ });
2441
2526
  return response;
2442
2527
  }
2443
2528
 
2444
2529
  if (response.headers?.get("status") === "error") {
2445
2530
  const json = safeJson(response).take();
2446
2531
  if (isErr(json)) {
2447
- return err(requestFailedTransportError({
2532
+ const error = requestFailedTransportError({
2448
2533
  code: "trellis.request.invalid_response",
2449
2534
  message: "Trellis returned an invalid response.",
2450
2535
  hint:
@@ -2452,12 +2537,19 @@ export class Trellis<
2452
2537
  method,
2453
2538
  subject,
2454
2539
  cause: json.error.cause,
2455
- }));
2540
+ });
2541
+ recordRuntimeError(error, {
2542
+ surface: "rpc",
2543
+ direction: "client",
2544
+ operation: method,
2545
+ phase: "response_decoding",
2546
+ });
2547
+ return err(error);
2456
2548
  }
2457
2549
 
2458
2550
  const errorData = parse(TrellisErrorDataSchema, json).take();
2459
2551
  if (isErr(errorData)) {
2460
- return err(requestFailedTransportError({
2552
+ const error = requestFailedTransportError({
2461
2553
  code: "trellis.request.invalid_response",
2462
2554
  message: "Trellis returned an invalid response.",
2463
2555
  hint:
@@ -2465,7 +2557,14 @@ export class Trellis<
2465
2557
  method,
2466
2558
  subject,
2467
2559
  cause: errorData.error,
2468
- }));
2560
+ });
2561
+ recordRuntimeError(error, {
2562
+ surface: "rpc",
2563
+ direction: "client",
2564
+ operation: method,
2565
+ phase: "response_decoding",
2566
+ });
2567
+ return err(error);
2469
2568
  }
2470
2569
 
2471
2570
  const declaredErrorTypes = Array.isArray(ctx.declaredErrorTypes)
@@ -2484,17 +2583,29 @@ export class Trellis<
2484
2583
  );
2485
2584
  if (reconstructed) {
2486
2585
  await this.#handleBrowserAuthRequired(reconstructed);
2586
+ recordRuntimeError(new RemoteError({ error: errorData }), {
2587
+ surface: "rpc",
2588
+ direction: "client",
2589
+ operation: method,
2590
+ phase: "remote_error",
2591
+ });
2487
2592
  return err(reconstructed);
2488
2593
  }
2489
2594
 
2490
2595
  const remoteError = new RemoteError({ error: errorData });
2491
2596
  await this.#handleBrowserAuthRequired(remoteError);
2597
+ recordRuntimeError(remoteError, {
2598
+ surface: "rpc",
2599
+ direction: "client",
2600
+ operation: method,
2601
+ phase: "remote_error",
2602
+ });
2492
2603
  return err(remoteError);
2493
2604
  }
2494
2605
 
2495
2606
  const json = safeJson(response).take();
2496
2607
  if (isErr(json)) {
2497
- return err(requestFailedTransportError({
2608
+ const error = requestFailedTransportError({
2498
2609
  code: "trellis.request.invalid_response",
2499
2610
  message: "Trellis returned an invalid response.",
2500
2611
  hint:
@@ -2502,11 +2613,24 @@ export class Trellis<
2502
2613
  method,
2503
2614
  subject,
2504
2615
  cause: json.error.cause,
2505
- }));
2616
+ });
2617
+ recordRuntimeError(error, {
2618
+ surface: "rpc",
2619
+ direction: "client",
2620
+ operation: method,
2621
+ phase: "response_decoding",
2622
+ });
2623
+ return err(error);
2506
2624
  }
2507
2625
 
2508
2626
  const outputResult = parseRuntimeSchema(ctx.output, json).take();
2509
2627
  if (isErr(outputResult)) {
2628
+ recordRuntimeError(outputResult.error, {
2629
+ surface: "rpc",
2630
+ direction: "client",
2631
+ operation: method,
2632
+ phase: "response_decoding",
2633
+ });
2510
2634
  return err(outputResult.error);
2511
2635
  }
2512
2636
 
@@ -2535,6 +2659,12 @@ export class Trellis<
2535
2659
  message: unexpected.message,
2536
2660
  });
2537
2661
  span.recordException(unexpected);
2662
+ recordRuntimeError(unexpected, {
2663
+ surface: "rpc",
2664
+ direction: "client",
2665
+ operation: method,
2666
+ phase: "unexpected",
2667
+ });
2538
2668
  return err(unexpected);
2539
2669
  } finally {
2540
2670
  span.end();
@@ -2683,13 +2813,29 @@ export class Trellis<
2683
2813
  ): AsyncResult<FeedSubscription<TEvent>, BaseError> {
2684
2814
  return AsyncResult.from((async () => {
2685
2815
  const payload = encodeRuntimeSchema(descriptor.input, input).take();
2686
- if (isErr(payload)) return payload;
2816
+ if (isErr(payload)) {
2817
+ recordRuntimeError(payload.error, {
2818
+ surface: "feed",
2819
+ direction: "client",
2820
+ operation: feed,
2821
+ phase: "request_encoding",
2822
+ });
2823
+ return payload;
2824
+ }
2687
2825
 
2688
2826
  const subject = this.template(
2689
2827
  descriptor.subject,
2690
2828
  input as Record<string, unknown>,
2691
2829
  ).take();
2692
- if (isErr(subject)) return subject;
2830
+ if (isErr(subject)) {
2831
+ recordRuntimeError(subject.error, {
2832
+ surface: "feed",
2833
+ direction: "client",
2834
+ operation: feed,
2835
+ phase: "request_template",
2836
+ });
2837
+ return subject;
2838
+ }
2693
2839
 
2694
2840
  const authHeaders = await this.#createProof(subject, payload);
2695
2841
  const headers = natsHeaders();
@@ -2711,14 +2857,21 @@ export class Trellis<
2711
2857
  } catch (cause) {
2712
2858
  opts?.signal?.removeEventListener("abort", abort);
2713
2859
  sub.unsubscribe();
2714
- return err(createTransportError({
2860
+ const error = createTransportError({
2715
2861
  code: "trellis.feed.subscribe_failed",
2716
2862
  message: "Trellis could not subscribe to the feed.",
2717
2863
  hint:
2718
2864
  "Retry the subscription. If it keeps failing, check Trellis runtime health.",
2719
2865
  cause,
2720
2866
  context: { feed, subject },
2721
- }));
2867
+ });
2868
+ recordRuntimeError(error, {
2869
+ surface: "feed",
2870
+ direction: "client",
2871
+ operation: feed,
2872
+ phase: "request_send",
2873
+ });
2874
+ return err(error);
2722
2875
  }
2723
2876
 
2724
2877
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
@@ -2749,7 +2902,7 @@ export class Trellis<
2749
2902
  if (firstFrame === "timeout" || firstFrame === "aborted") {
2750
2903
  opts?.signal?.removeEventListener("abort", abort);
2751
2904
  sub.unsubscribe();
2752
- return err(createTransportError({
2905
+ const error = createTransportError({
2753
2906
  code: firstFrame === "timeout"
2754
2907
  ? "trellis.feed.subscribe_timeout"
2755
2908
  : "trellis.feed.subscribe_aborted",
@@ -2760,30 +2913,51 @@ export class Trellis<
2760
2913
  ? "Check that the target service is running and has the current deployment digest, then retry."
2761
2914
  : "Retry the subscription if the feed is still needed.",
2762
2915
  context: { feed, subject },
2763
- }));
2916
+ });
2917
+ recordRuntimeError(error, {
2918
+ surface: "feed",
2919
+ direction: "client",
2920
+ operation: feed,
2921
+ phase: "handshake",
2922
+ });
2923
+ return err(error);
2764
2924
  }
2765
2925
  if (firstFrame.done) {
2766
2926
  opts?.signal?.removeEventListener("abort", abort);
2767
2927
  sub.unsubscribe();
2768
- return err(createTransportError({
2928
+ const error = createTransportError({
2769
2929
  code: "trellis.feed.subscribe_closed",
2770
2930
  message: "Trellis closed the feed before acknowledging it.",
2771
2931
  hint:
2772
2932
  "Retry the subscription. If it keeps failing, check Trellis runtime health.",
2773
2933
  context: { feed, subject },
2774
- }));
2934
+ });
2935
+ recordRuntimeError(error, {
2936
+ surface: "feed",
2937
+ direction: "client",
2938
+ operation: feed,
2939
+ phase: "handshake",
2940
+ });
2941
+ return err(error);
2775
2942
  }
2776
2943
  const firstMessage = firstFrame.value;
2777
2944
  if (firstMessage.headers?.get("status") === "error") {
2778
2945
  opts?.signal?.removeEventListener("abort", abort);
2779
2946
  sub.unsubscribe();
2780
- return err(createTransportError({
2947
+ const error = createTransportError({
2781
2948
  code: "trellis.feed.failed",
2782
2949
  message: "Trellis rejected the feed subscription.",
2783
2950
  hint:
2784
2951
  "Retry the subscription. If it keeps failing, check Trellis runtime health and permissions.",
2785
2952
  context: { feed, subject, frame: firstMessage.string() },
2786
- }));
2953
+ });
2954
+ recordRuntimeError(error, {
2955
+ surface: "feed",
2956
+ direction: "client",
2957
+ operation: feed,
2958
+ phase: "remote_error",
2959
+ });
2960
+ return err(error);
2787
2961
  }
2788
2962
  const firstEvent = firstMessage.headers?.get("feed-status") === "ready"
2789
2963
  ? undefined
@@ -2794,18 +2968,41 @@ export class Trellis<
2794
2968
  try {
2795
2969
  const parseFeedFrame = (msg: Msg): TEvent => {
2796
2970
  if (msg.headers?.get("status") === "error") {
2797
- throw createTransportError({
2971
+ const error = createTransportError({
2798
2972
  code: "trellis.feed.failed",
2799
2973
  message: "Trellis stopped the feed.",
2800
2974
  hint:
2801
2975
  "Retry the subscription. If it keeps failing, check Trellis runtime health.",
2802
2976
  context: { feed, subject, frame: msg.string() },
2803
2977
  });
2978
+ recordRuntimeError(error, {
2979
+ surface: "feed",
2980
+ direction: "client",
2981
+ operation: feed,
2982
+ phase: "remote_error",
2983
+ });
2984
+ throw error;
2804
2985
  }
2805
2986
  const json = safeJson(msg).take();
2806
- if (isErr(json)) throw json.error;
2987
+ if (isErr(json)) {
2988
+ recordRuntimeError(json.error, {
2989
+ surface: "feed",
2990
+ direction: "client",
2991
+ operation: feed,
2992
+ phase: "event_decoding",
2993
+ });
2994
+ throw json.error;
2995
+ }
2807
2996
  const parsed = parseRuntimeSchema(eventSchema, json).take();
2808
- if (isErr(parsed)) throw parsed.error;
2997
+ if (isErr(parsed)) {
2998
+ recordRuntimeError(parsed.error, {
2999
+ surface: "feed",
3000
+ direction: "client",
3001
+ operation: feed,
3002
+ phase: "event_validation",
3003
+ });
3004
+ throw parsed.error;
3005
+ }
2809
3006
  return parsed as TEvent;
2810
3007
  };
2811
3008
  if (firstEvent) yield parseFeedFrame(firstEvent);
@@ -2836,7 +3033,7 @@ export class Trellis<
2836
3033
  sub = this.nats.subscribe(subject);
2837
3034
  await this.nats.flush();
2838
3035
  } catch (cause) {
2839
- throw createTransportError({
3036
+ const error = createTransportError({
2840
3037
  code: "trellis.feed.listen_failed",
2841
3038
  message: "Trellis could not listen for feed requests.",
2842
3039
  hint:
@@ -2844,6 +3041,13 @@ export class Trellis<
2844
3041
  cause,
2845
3042
  context: { feed, subject },
2846
3043
  });
3044
+ recordRuntimeError(error, {
3045
+ surface: "feed",
3046
+ direction: "server",
3047
+ operation: feed,
3048
+ phase: "listen",
3049
+ });
3050
+ throw error;
2847
3051
  }
2848
3052
  const task = AsyncResult.try(async () => {
2849
3053
  for await (const msg of sub) {
@@ -2860,9 +3064,20 @@ export class Trellis<
2860
3064
  this.#respondWithError(msg, value.error);
2861
3065
  }
2862
3066
  } catch (cause) {
2863
- const error = cause instanceof BaseError
2864
- ? cause
2865
- : new UnexpectedError({ cause });
3067
+ const error = annotateHandlerBoundaryError(cause, {
3068
+ feed,
3069
+ requestId: msg.headers?.get("request-id"),
3070
+ service: this.name,
3071
+ contractId: this.contractId,
3072
+ contractDigest: this.contractDigest,
3073
+ traceId: traceIdFromTraceparent(msg.headers?.get("traceparent")),
3074
+ });
3075
+ recordRuntimeError(error, {
3076
+ surface: "feed",
3077
+ direction: "server",
3078
+ operation: feed,
3079
+ phase: "handler_throw",
3080
+ });
2866
3081
  this.#respondWithError(msg, error);
2867
3082
  }
2868
3083
  })();
@@ -2880,9 +3095,25 @@ export class Trellis<
2880
3095
  ) => unknown | Promise<unknown>,
2881
3096
  ): Promise<Result<void, BaseError>> {
2882
3097
  const json = safeJson(msg).take();
2883
- if (isErr(json)) return json;
3098
+ if (isErr(json)) {
3099
+ recordRuntimeError(json.error, {
3100
+ surface: "feed",
3101
+ direction: "server",
3102
+ operation: feed,
3103
+ phase: "request_decoding",
3104
+ });
3105
+ return json;
3106
+ }
2884
3107
  const parsed = parseRuntimeSchema(descriptor.input, json).take();
2885
- if (isErr(parsed)) return parsed;
3108
+ if (isErr(parsed)) {
3109
+ recordRuntimeError(parsed.error, {
3110
+ surface: "feed",
3111
+ direction: "server",
3112
+ operation: feed,
3113
+ phase: "input_validation",
3114
+ });
3115
+ return parsed;
3116
+ }
2886
3117
 
2887
3118
  const caller = await this.#authenticateFeedRequest({
2888
3119
  feed,
@@ -2892,13 +3123,26 @@ export class Trellis<
2892
3123
  requiredCapabilities: descriptor.subscribeCapabilities,
2893
3124
  });
2894
3125
  const callerValue = caller.take();
2895
- if (isErr(callerValue)) return callerValue;
3126
+ if (isErr(callerValue)) {
3127
+ recordRuntimeError(callerValue.error, {
3128
+ surface: "feed",
3129
+ direction: "server",
3130
+ operation: feed,
3131
+ phase: "auth",
3132
+ });
3133
+ return callerValue;
3134
+ }
2896
3135
  if (!msg.reply) {
2897
- return err(
2898
- new UnexpectedError({
2899
- context: { feed, reason: "missing_reply" },
2900
- }),
2901
- );
3136
+ const error = new UnexpectedError({
3137
+ context: { feed, reason: "missing_reply" },
3138
+ });
3139
+ recordRuntimeError(error, {
3140
+ surface: "feed",
3141
+ direction: "server",
3142
+ operation: feed,
3143
+ phase: "handshake",
3144
+ });
3145
+ return err(error);
2902
3146
  }
2903
3147
  const readyHeaders = natsHeaders();
2904
3148
  readyHeaders.set("feed-status", "ready");
@@ -2907,26 +3151,73 @@ export class Trellis<
2907
3151
 
2908
3152
  const controller = new AbortController();
2909
3153
  try {
2910
- await handler({
3154
+ const handlerResult = await handler({
2911
3155
  input: parsed as TInput,
2912
3156
  caller: callerValue,
2913
3157
  signal: controller.signal,
2914
3158
  emit: (event: TEvent) =>
2915
3159
  AsyncResult.from((async () => {
2916
3160
  const payload = encodeRuntimeSchema(descriptor.event, event).take();
2917
- if (isErr(payload)) return payload;
3161
+ if (isErr(payload)) {
3162
+ recordRuntimeError(payload.error, {
3163
+ surface: "feed",
3164
+ direction: "server",
3165
+ operation: feed,
3166
+ phase: "event_encoding",
3167
+ });
3168
+ return payload;
3169
+ }
2918
3170
  if (!msg.reply) {
2919
- return err(
2920
- new UnexpectedError({
2921
- context: { feed, reason: "missing_reply" },
2922
- }),
2923
- );
3171
+ const error = new UnexpectedError({
3172
+ context: { feed, reason: "missing_reply" },
3173
+ });
3174
+ recordRuntimeError(error, {
3175
+ surface: "feed",
3176
+ direction: "server",
3177
+ operation: feed,
3178
+ phase: "event_publish",
3179
+ });
3180
+ return err(error);
3181
+ }
3182
+ try {
3183
+ this.nats.publish(msg.reply, payload);
3184
+ await this.nats.flush();
3185
+ } catch (cause) {
3186
+ const error = new UnexpectedError({
3187
+ cause,
3188
+ context: { feed },
3189
+ });
3190
+ recordRuntimeError(error, {
3191
+ surface: "feed",
3192
+ direction: "server",
3193
+ operation: feed,
3194
+ phase: "event_publish",
3195
+ });
3196
+ return err(error);
2924
3197
  }
2925
- this.nats.publish(msg.reply, payload);
2926
- await this.nats.flush();
2927
3198
  return ok(undefined);
2928
3199
  })()),
2929
3200
  });
3201
+ const handlerOutcome = isResultLike(handlerResult)
3202
+ ? handlerResult.take()
3203
+ : handlerResult;
3204
+ if (isErr(handlerOutcome)) {
3205
+ const error = annotateHandlerBoundaryError(handlerOutcome.error, {
3206
+ feed,
3207
+ requestId: msg.headers?.get("request-id"),
3208
+ service: this.name,
3209
+ contractId: this.contractId,
3210
+ contractDigest: this.contractDigest,
3211
+ traceId: traceIdFromTraceparent(msg.headers?.get("traceparent")),
3212
+ });
3213
+ recordRuntimeError(error, {
3214
+ surface: "feed",
3215
+ direction: "server",
3216
+ operation: feed,
3217
+ phase: "handler_result",
3218
+ });
3219
+ return err(error);
3220
+ }
2930
3221
  return ok(undefined);
2931
3222
  } finally {
2932
3223
  controller.abort();
@@ -3113,6 +3404,12 @@ export class Trellis<
3113
3404
  code: SpanStatusCode.ERROR,
3114
3405
  message: "Failed to parse JSON",
3115
3406
  });
3407
+ recordRuntimeError(jsonData.error, {
3408
+ surface: "rpc",
3409
+ direction: "server",
3410
+ operation: String(method),
3411
+ phase: "parse",
3412
+ });
3116
3413
  return jsonData;
3117
3414
  }
3118
3415
 
@@ -3122,6 +3419,12 @@ export class Trellis<
3122
3419
  code: SpanStatusCode.ERROR,
3123
3420
  message: "Input validation failed",
3124
3421
  });
3422
+ recordRuntimeError(parsedInput.error, {
3423
+ surface: "rpc",
3424
+ direction: "server",
3425
+ operation: String(method),
3426
+ phase: "input_validation",
3427
+ });
3125
3428
  return parsedInput;
3126
3429
  }
3127
3430
 
@@ -3148,7 +3451,14 @@ export class Trellis<
3148
3451
  code: SpanStatusCode.ERROR,
3149
3452
  message: "Missing session-key",
3150
3453
  });
3151
- return err(new AuthError({ reason: "missing_session_key" }));
3454
+ const error = new AuthError({ reason: "missing_session_key" });
3455
+ recordRuntimeError(error, {
3456
+ surface: "rpc",
3457
+ direction: "server",
3458
+ operation: String(method),
3459
+ phase: "auth",
3460
+ });
3461
+ return err(error);
3152
3462
  }
3153
3463
  if (!proof) {
3154
3464
  this.#log.warn({ method }, "Missing proof in request");
@@ -3156,11 +3466,25 @@ export class Trellis<
3156
3466
  code: SpanStatusCode.ERROR,
3157
3467
  message: "Missing proof",
3158
3468
  });
3159
- return err(new AuthError({ reason: "missing_proof" }));
3469
+ const error = new AuthError({ reason: "missing_proof" });
3470
+ recordRuntimeError(error, {
3471
+ surface: "rpc",
3472
+ direction: "server",
3473
+ operation: String(method),
3474
+ phase: "auth",
3475
+ });
3476
+ return err(error);
3160
3477
  }
3161
3478
  const iat = Number(iatHeader);
3162
3479
  if (!Number.isSafeInteger(iat) || !requestId) {
3163
- return err(new AuthError({ reason: "invalid_signature" }));
3480
+ const error = new AuthError({ reason: "invalid_signature" });
3481
+ recordRuntimeError(error, {
3482
+ surface: "rpc",
3483
+ direction: "server",
3484
+ operation: String(method),
3485
+ phase: "auth",
3486
+ });
3487
+ return err(error);
3164
3488
  }
3165
3489
 
3166
3490
  // Verify proof signature locally using the raw request bytes we received.
@@ -3199,12 +3523,17 @@ export class Trellis<
3199
3523
  code: SpanStatusCode.ERROR,
3200
3524
  message: "Invalid signature",
3201
3525
  });
3202
- return err(
3203
- new AuthError({
3204
- reason: "invalid_signature",
3205
- context: { sessionKey },
3206
- }),
3207
- );
3526
+ const error = new AuthError({
3527
+ reason: "invalid_signature",
3528
+ context: { sessionKey },
3529
+ });
3530
+ recordRuntimeError(error, {
3531
+ surface: "rpc",
3532
+ direction: "server",
3533
+ operation: String(method),
3534
+ phase: "auth",
3535
+ });
3536
+ return err(error);
3208
3537
  }
3209
3538
 
3210
3539
  let auth:
@@ -3250,11 +3579,16 @@ export class Trellis<
3250
3579
  }
3251
3580
 
3252
3581
  if (!auth) {
3253
- return err(
3254
- new UnexpectedError({
3255
- context: { reason: "missing_auth_validate_result" },
3256
- }),
3257
- );
3582
+ const error = new UnexpectedError({
3583
+ context: { reason: "missing_auth_validate_result" },
3584
+ });
3585
+ recordRuntimeError(error, {
3586
+ surface: "rpc",
3587
+ direction: "server",
3588
+ operation: String(method),
3589
+ phase: "auth",
3590
+ });
3591
+ return err(error);
3258
3592
  }
3259
3593
 
3260
3594
  if (auth instanceof Error) {
@@ -3274,9 +3608,22 @@ export class Trellis<
3274
3608
  message: "Auth.Requests.Validate failed",
3275
3609
  });
3276
3610
  if (auth instanceof BaseError) {
3611
+ recordRuntimeError(auth, {
3612
+ surface: "rpc",
3613
+ direction: "server",
3614
+ operation: String(method),
3615
+ phase: "auth",
3616
+ });
3277
3617
  return err(auth);
3278
3618
  }
3279
- return err(new UnexpectedError({ cause: auth }));
3619
+ const error = new UnexpectedError({ cause: auth });
3620
+ recordRuntimeError(error, {
3621
+ surface: "rpc",
3622
+ direction: "server",
3623
+ operation: String(method),
3624
+ phase: "auth",
3625
+ });
3626
+ return err(error);
3280
3627
  }
3281
3628
 
3282
3629
  if (!auth.allowed) {
@@ -3284,15 +3631,20 @@ export class Trellis<
3284
3631
  code: SpanStatusCode.ERROR,
3285
3632
  message: "Insufficient permissions",
3286
3633
  });
3287
- return err(
3288
- new AuthError({
3289
- reason: "insufficient_permissions",
3290
- context: {
3291
- requiredCapabilities: ctx.callerCapabilities,
3292
- userCapabilities: auth.caller.capabilities,
3293
- },
3294
- }),
3295
- );
3634
+ const error = new AuthError({
3635
+ reason: "insufficient_permissions",
3636
+ context: {
3637
+ requiredCapabilities: ctx.callerCapabilities,
3638
+ userCapabilities: auth.caller.capabilities,
3639
+ },
3640
+ });
3641
+ recordRuntimeError(error, {
3642
+ surface: "rpc",
3643
+ direction: "server",
3644
+ operation: String(method),
3645
+ phase: "auth",
3646
+ });
3647
+ return err(error);
3296
3648
  }
3297
3649
 
3298
3650
  if (
@@ -3303,12 +3655,17 @@ export class Trellis<
3303
3655
  code: SpanStatusCode.ERROR,
3304
3656
  message: "Reply subject mismatch",
3305
3657
  });
3306
- return err(
3307
- new AuthError({
3308
- reason: "reply_subject_mismatch",
3309
- context: { expected: auth.inboxPrefix, actual: msg.reply },
3310
- }),
3311
- );
3658
+ const error = new AuthError({
3659
+ reason: "reply_subject_mismatch",
3660
+ context: { expected: auth.inboxPrefix, actual: msg.reply },
3661
+ });
3662
+ recordRuntimeError(error, {
3663
+ surface: "rpc",
3664
+ direction: "server",
3665
+ operation: String(method),
3666
+ phase: "auth",
3667
+ });
3668
+ return err(error);
3312
3669
  }
3313
3670
 
3314
3671
  caller = auth.caller;
@@ -3350,7 +3707,17 @@ export class Trellis<
3350
3707
  );
3351
3708
 
3352
3709
  if (handlerResultWrapped.isErr()) {
3353
- const error = handlerResultWrapped.error.withContext({ method });
3710
+ const error = annotateHandlerBoundaryError(
3711
+ handlerResultWrapped.error,
3712
+ {
3713
+ method: String(method),
3714
+ requestId: msg.headers?.get("request-id"),
3715
+ service: this.name,
3716
+ contractId: this.contractId,
3717
+ contractDigest: this.contractDigest,
3718
+ traceId: activeTraceId(span) ?? incomingTraceId,
3719
+ },
3720
+ );
3354
3721
  this.#log.error(
3355
3722
  {
3356
3723
  method,
@@ -3366,6 +3733,12 @@ export class Trellis<
3366
3733
  message: error.message,
3367
3734
  });
3368
3735
  span.recordException(error);
3736
+ recordRuntimeError(error, {
3737
+ surface: "rpc",
3738
+ direction: "server",
3739
+ operation: String(method),
3740
+ phase: "handler_throw",
3741
+ });
3369
3742
  return err(error);
3370
3743
  }
3371
3744
 
@@ -3374,12 +3747,14 @@ export class Trellis<
3374
3747
  };
3375
3748
  const handlerOutcome = handlerResult.take();
3376
3749
  if (isErr(handlerOutcome)) {
3377
- const handlerError = handlerOutcome.error;
3378
-
3379
- const error = handlerError instanceof BaseError &&
3380
- !(handlerError instanceof RemoteError)
3381
- ? handlerError
3382
- : new UnexpectedError({ cause: handlerError });
3750
+ const error = annotateHandlerBoundaryError(handlerOutcome.error, {
3751
+ method: String(method),
3752
+ requestId: msg.headers?.get("request-id"),
3753
+ service: this.name,
3754
+ contractId: this.contractId,
3755
+ contractDigest: this.contractDigest,
3756
+ traceId: activeTraceId(span) ?? incomingTraceId,
3757
+ });
3383
3758
 
3384
3759
  this.#log.error(
3385
3760
  {
@@ -3396,6 +3771,12 @@ export class Trellis<
3396
3771
  code: SpanStatusCode.ERROR,
3397
3772
  message: error.message,
3398
3773
  });
3774
+ recordRuntimeError(error, {
3775
+ surface: "rpc",
3776
+ direction: "server",
3777
+ operation: String(method),
3778
+ phase: "handler_result",
3779
+ });
3399
3780
  return err(error);
3400
3781
  }
3401
3782
 
@@ -3405,6 +3786,12 @@ export class Trellis<
3405
3786
  code: SpanStatusCode.ERROR,
3406
3787
  message: "Output encoding failed",
3407
3788
  });
3789
+ recordRuntimeError(encoded.error, {
3790
+ surface: "rpc",
3791
+ direction: "server",
3792
+ operation: String(method),
3793
+ phase: "output_encoding",
3794
+ });
3408
3795
  return encoded;
3409
3796
  }
3410
3797
 
@@ -3529,17 +3916,28 @@ export class Trellis<
3529
3916
  typeof eventName
3530
3917
  >;
3531
3918
  if (!ctx) {
3532
- return err(
3533
- new UnexpectedError({
3534
- cause: this.#unknownApiError("event", event.toString()),
3535
- context: { event: event.toString() },
3536
- }),
3537
- );
3919
+ const error = new UnexpectedError({
3920
+ cause: this.#unknownApiError("event", event.toString()),
3921
+ context: { event: event.toString() },
3922
+ });
3923
+ recordRuntimeError(error, {
3924
+ surface: "event",
3925
+ direction: "publisher",
3926
+ operation: event,
3927
+ phase: "prepare",
3928
+ });
3929
+ return err(error);
3538
3930
  }
3539
3931
 
3540
3932
  const subject = this.template(ctx.subject, data).take();
3541
3933
  if (isErr(subject)) {
3542
3934
  logger.error({ err: subject.error }, "Failed to template event.");
3935
+ recordRuntimeError(subject.error, {
3936
+ surface: "event",
3937
+ direction: "publisher",
3938
+ operation: event,
3939
+ phase: "request_encoding",
3940
+ });
3543
3941
  return subject;
3544
3942
  }
3545
3943
 
@@ -3554,7 +3952,14 @@ export class Trellis<
3554
3952
  const msg = encodeSchema(ctx.event, payload).take();
3555
3953
  if (isErr(msg)) {
3556
3954
  logger.error({ err: msg.error }, "Failed to encode event.");
3557
- return err(new UnexpectedError({ cause: msg.error }));
3955
+ const error = new UnexpectedError({ cause: msg.error });
3956
+ recordRuntimeError(error, {
3957
+ surface: "event",
3958
+ direction: "publisher",
3959
+ operation: event,
3960
+ phase: "request_encoding",
3961
+ });
3962
+ return err(error);
3558
3963
  }
3559
3964
 
3560
3965
  const headers = natsHeaders();
@@ -3574,9 +3979,17 @@ export class Trellis<
3574
3979
  headers: Object.freeze(headerRecord),
3575
3980
  }));
3576
3981
  } catch (cause) {
3577
- return err(
3578
- new UnexpectedError({ cause, context: { event: event.toString() } }),
3579
- );
3982
+ const error = new UnexpectedError({
3983
+ cause,
3984
+ context: { event: event.toString() },
3985
+ });
3986
+ recordRuntimeError(error, {
3987
+ surface: "event",
3988
+ direction: "publisher",
3989
+ operation: event,
3990
+ phase: "prepare",
3991
+ });
3992
+ return err(error);
3580
3993
  }
3581
3994
  }
3582
3995
 
@@ -3601,9 +4014,17 @@ export class Trellis<
3601
4014
  await this.js.publish(event.subject, event.encodedPayload, { headers });
3602
4015
  return ok(undefined);
3603
4016
  } catch (cause) {
3604
- return err(
3605
- new UnexpectedError({ cause, context: { event: event.event } }),
3606
- );
4017
+ const error = new UnexpectedError({
4018
+ cause,
4019
+ context: { event: event.event },
4020
+ });
4021
+ recordRuntimeError(error, {
4022
+ surface: "event",
4023
+ direction: "publisher",
4024
+ operation: event.event,
4025
+ phase: "publish",
4026
+ });
4027
+ return err(error);
3607
4028
  }
3608
4029
  })());
3609
4030
  }
@@ -3710,24 +4131,33 @@ export class Trellis<
3710
4131
  const m = parsedEvent.take();
3711
4132
  if (isErr(m)) {
3712
4133
  this.#log.error({ error: m.error }, "Event validation failed");
4134
+ recordRuntimeError(m.error, {
4135
+ surface: "event",
4136
+ direction: "consumer",
4137
+ operation: String(event),
4138
+ phase: "input_validation",
4139
+ });
3713
4140
  continue;
3714
4141
  }
3715
4142
 
3716
- const handlerResult = await AsyncResult.lift(
3717
- fn(
3718
- m as EventOf<TA, EventsOf<TA>>,
3719
- createEventListenerContext({
3720
- payload: m,
3721
- subject: msg.subject,
3722
- mode: "ephemeral",
3723
- message: msg,
3724
- }),
3725
- ),
3726
- );
3727
- if (handlerResult.isErr()) {
4143
+ const handlerResult = await this.#invokeEventHandler({
4144
+ event,
4145
+ payload: m,
4146
+ mode: "ephemeral",
4147
+ message: msg,
4148
+ fn,
4149
+ });
4150
+ const handlerValue = handlerResult.take();
4151
+ if (isErr(handlerValue)) {
4152
+ recordRuntimeError(handlerValue.error, {
4153
+ surface: "event",
4154
+ direction: "consumer",
4155
+ operation: String(event),
4156
+ phase: "handler_result",
4157
+ });
3728
4158
  this.#log.error(
3729
4159
  {
3730
- error: handlerResult.error.toSerializable(),
4160
+ error: handlerValue.error.toSerializable(),
3731
4161
  event,
3732
4162
  subject: msg.subject,
3733
4163
  },
@@ -3741,6 +4171,42 @@ export class Trellis<
3741
4171
  return ok(undefined);
3742
4172
  }
3743
4173
 
4174
+ async #invokeEventHandler(args: {
4175
+ event: EventsOf<TA>;
4176
+ payload: unknown;
4177
+ mode: "durable" | "ephemeral";
4178
+ group?: string;
4179
+ message: Pick<Msg, "headers" | "subject"> & object;
4180
+ fn: EventCallback<EventOf<TA, EventsOf<TA>>>;
4181
+ }): Promise<Result<void, BaseError>> {
4182
+ const annotation = {
4183
+ event: String(args.event),
4184
+ service: this.name,
4185
+ contractId: this.contractId,
4186
+ contractDigest: this.contractDigest,
4187
+ traceId: traceIdFromTraceparent(args.message.headers?.get("traceparent")),
4188
+ };
4189
+ try {
4190
+ const result = await Promise.resolve(args.fn(
4191
+ args.payload as EventOf<TA, EventsOf<TA>>,
4192
+ createEventListenerContext({
4193
+ payload: args.payload,
4194
+ subject: args.message.subject,
4195
+ mode: args.mode,
4196
+ ...(args.group ? { group: args.group } : {}),
4197
+ message: args.message,
4198
+ }),
4199
+ ));
4200
+ const outcome = isResultLike(result) ? result.take() : result;
4201
+ if (isErr(outcome)) {
4202
+ return err(annotateHandlerBoundaryError(outcome.error, annotation));
4203
+ }
4204
+ return ok(undefined);
4205
+ } catch (cause) {
4206
+ return err(annotateHandlerBoundaryError(cause, annotation));
4207
+ }
4208
+ }
4209
+
3744
4210
  #resolveEventConsumerGroup(
3745
4211
  event: EventsOf<TA>,
3746
4212
  opts: EventOpts | undefined,
@@ -3944,21 +4410,18 @@ export class Trellis<
3944
4410
  continue;
3945
4411
  }
3946
4412
 
3947
- const handlerResult = await AsyncResult.lift(
3948
- fn(
3949
- m as EventOf<TA, EventsOf<TA>>,
3950
- createEventListenerContext({
3951
- payload: m,
3952
- subject: msg.subject,
3953
- mode: "durable",
3954
- message: msg,
3955
- }),
3956
- ),
3957
- );
3958
- if (handlerResult.isErr()) {
4413
+ const handlerResult = await this.#invokeEventHandler({
4414
+ event,
4415
+ payload: m,
4416
+ mode: "durable",
4417
+ message: msg,
4418
+ fn,
4419
+ });
4420
+ const handlerValue = handlerResult.take();
4421
+ if (isErr(handlerValue)) {
3959
4422
  this.#log.error(
3960
4423
  {
3961
- error: handlerResult.error.toSerializable(),
4424
+ error: handlerValue.error.toSerializable(),
3962
4425
  event,
3963
4426
  subject: msg.subject,
3964
4427
  },
@@ -4009,27 +4472,36 @@ export class Trellis<
4009
4472
  { error: eventPayload.error },
4010
4473
  "Event validation failed",
4011
4474
  );
4475
+ recordRuntimeError(eventPayload.error, {
4476
+ surface: "event",
4477
+ direction: "consumer",
4478
+ operation: String(registration.event),
4479
+ phase: "input_validation",
4480
+ });
4012
4481
  msg.term();
4013
4482
  failed = true;
4014
4483
  break;
4015
4484
  }
4016
4485
 
4017
- const handlerResult = await AsyncResult.lift(
4018
- registration.fn(
4019
- eventPayload as EventOf<TA, EventsOf<TA>>,
4020
- createEventListenerContext({
4021
- payload: eventPayload,
4022
- subject: msg.subject,
4023
- mode: "durable",
4024
- group,
4025
- message: msg,
4026
- }),
4027
- ),
4028
- );
4029
- if (handlerResult.isErr()) {
4486
+ const handlerResult = await this.#invokeEventHandler({
4487
+ event: registration.event,
4488
+ payload: eventPayload,
4489
+ mode: "durable",
4490
+ group,
4491
+ message: msg,
4492
+ fn: registration.fn,
4493
+ });
4494
+ const handlerValue = handlerResult.take();
4495
+ if (isErr(handlerValue)) {
4496
+ recordRuntimeError(handlerValue.error, {
4497
+ surface: "event",
4498
+ direction: "consumer",
4499
+ operation: String(registration.event),
4500
+ phase: "handler_result",
4501
+ });
4030
4502
  this.#log.error(
4031
4503
  {
4032
- error: handlerResult.error.toSerializable(),
4504
+ error: handlerValue.error.toSerializable(),
4033
4505
  event: registration.event,
4034
4506
  subject: msg.subject,
4035
4507
  },
@@ -4236,6 +4708,12 @@ export class Trellis<
4236
4708
  code: SpanStatusCode.ERROR,
4237
4709
  message: response.error.message,
4238
4710
  });
4711
+ recordRuntimeError(response.error, {
4712
+ surface: "operation",
4713
+ direction: "client",
4714
+ operation: "requestJson",
4715
+ phase: "request_send",
4716
+ });
4239
4717
  return response;
4240
4718
  }
4241
4719
 
@@ -4253,6 +4731,12 @@ export class Trellis<
4253
4731
  code: SpanStatusCode.ERROR,
4254
4732
  message: error.message,
4255
4733
  });
4734
+ recordRuntimeError(error, {
4735
+ surface: "operation",
4736
+ direction: "client",
4737
+ operation: "requestJson",
4738
+ phase: "response_decoding",
4739
+ });
4256
4740
  return err(error);
4257
4741
  }
4258
4742
 
@@ -4265,6 +4749,12 @@ export class Trellis<
4265
4749
  message: error.message,
4266
4750
  });
4267
4751
  span.recordException(error);
4752
+ recordRuntimeError(error, {
4753
+ surface: "operation",
4754
+ direction: "client",
4755
+ operation: "requestJson",
4756
+ phase: "unexpected",
4757
+ });
4268
4758
  return err(error);
4269
4759
  } finally {
4270
4760
  span.end();
@@ -4301,40 +4791,61 @@ export class Trellis<
4301
4791
  await this.nats.flush();
4302
4792
  } catch (cause) {
4303
4793
  sub.unsubscribe();
4304
- return err(createTransportError({
4794
+ const error = createTransportError({
4305
4795
  code: "trellis.watch.failed",
4306
4796
  message: "Trellis could not start the operation watch.",
4307
4797
  hint:
4308
4798
  "Retry watching the operation. If it keeps failing, reconnect to Trellis and try again.",
4309
4799
  cause,
4310
4800
  context: { subject },
4311
- }));
4801
+ });
4802
+ recordRuntimeError(error, {
4803
+ surface: "operation",
4804
+ direction: "client",
4805
+ operation: "watchJson",
4806
+ phase: "request_send",
4807
+ });
4808
+ return err(error);
4312
4809
  }
4313
4810
 
4314
4811
  return ok((async function* () {
4315
4812
  try {
4316
4813
  for await (const msg of sub) {
4317
4814
  if (msg.headers?.get("status") === "error") {
4318
- yield err(createTransportError({
4815
+ const error = createTransportError({
4319
4816
  code: "trellis.watch.failed",
4320
4817
  message: "Trellis stopped the operation watch.",
4321
4818
  hint:
4322
4819
  "Retry watching the operation. If it keeps happening, reconnect to Trellis and try again.",
4323
4820
  context: { subject, frame: msg.string() },
4324
- }));
4821
+ });
4822
+ recordRuntimeError(error, {
4823
+ surface: "operation",
4824
+ direction: "client",
4825
+ operation: "watchJson",
4826
+ phase: "remote_error",
4827
+ });
4828
+ yield err(error);
4325
4829
  continue;
4326
4830
  }
4327
4831
 
4328
4832
  const json = safeJson(msg).take();
4329
4833
  if (isErr(json)) {
4330
- yield err(createTransportError({
4834
+ const error = createTransportError({
4331
4835
  code: "trellis.watch.invalid_response",
4332
4836
  message: "Trellis returned an invalid watch update.",
4333
4837
  hint:
4334
4838
  "Retry watching the operation. If it keeps happening, reconnect to Trellis and try again.",
4335
4839
  cause: json.error.cause,
4336
4840
  context: { subject },
4337
- }));
4841
+ });
4842
+ recordRuntimeError(error, {
4843
+ surface: "operation",
4844
+ direction: "client",
4845
+ operation: "watchJson",
4846
+ phase: "response_decoding",
4847
+ });
4848
+ yield err(error);
4338
4849
  continue;
4339
4850
  }
4340
4851