@qlever-llc/trellis 0.10.11 → 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 (110) hide show
  1. package/esm/errors/TrellisError.d.ts +3 -3
  2. package/esm/errors/TrellisError.js +3 -3
  3. package/esm/server/internal_jobs/job-manager.d.ts.map +1 -1
  4. package/esm/server/internal_jobs/job-manager.js +32 -1
  5. package/esm/server/runtime.d.ts +3 -0
  6. package/esm/server/runtime.d.ts.map +1 -1
  7. package/esm/server/service.d.ts +15 -0
  8. package/esm/server/service.d.ts.map +1 -1
  9. package/esm/server/service.js +8 -0
  10. package/esm/server.d.ts.map +1 -1
  11. package/esm/server.js +54 -6
  12. package/esm/service/deno.d.ts +1 -1
  13. package/esm/service/deno.d.ts.map +1 -1
  14. package/esm/service/mod.d.ts +1 -1
  15. package/esm/service/mod.d.ts.map +1 -1
  16. package/esm/service/node.d.ts +1 -1
  17. package/esm/service/node.d.ts.map +1 -1
  18. package/esm/service/outbox_inbox.d.ts.map +1 -1
  19. package/esm/service/outbox_inbox.js +14 -0
  20. package/esm/telemetry/core.d.ts.map +1 -1
  21. package/esm/telemetry/core.js +1 -1
  22. package/esm/telemetry/env.d.ts.map +1 -1
  23. package/esm/telemetry/env.js +6 -1
  24. package/esm/telemetry/init.d.ts +3 -0
  25. package/esm/telemetry/init.d.ts.map +1 -0
  26. package/esm/telemetry/init.js +7 -0
  27. package/esm/telemetry/metrics.d.ts +34 -0
  28. package/esm/telemetry/metrics.d.ts.map +1 -0
  29. package/esm/telemetry/metrics.js +181 -0
  30. package/esm/telemetry/mod.d.ts +3 -0
  31. package/esm/telemetry/mod.d.ts.map +1 -1
  32. package/esm/telemetry/mod.js +2 -0
  33. package/esm/telemetry/runtime.d.ts +2 -0
  34. package/esm/telemetry/runtime.d.ts.map +1 -0
  35. package/esm/telemetry/runtime.js +134 -0
  36. package/esm/telemetry.d.ts +3 -0
  37. package/esm/telemetry.d.ts.map +1 -0
  38. package/esm/telemetry.js +2 -0
  39. package/esm/transfer.d.ts.map +1 -1
  40. package/esm/transfer.js +27 -16
  41. package/esm/trellis.d.ts.map +1 -1
  42. package/esm/trellis.js +460 -56
  43. package/package.json +7 -5
  44. package/script/errors/TrellisError.d.ts +3 -3
  45. package/script/errors/TrellisError.js +3 -3
  46. package/script/server/internal_jobs/job-manager.d.ts.map +1 -1
  47. package/script/server/internal_jobs/job-manager.js +32 -1
  48. package/script/server/runtime.d.ts +3 -0
  49. package/script/server/runtime.d.ts.map +1 -1
  50. package/script/server/service.d.ts +15 -0
  51. package/script/server/service.d.ts.map +1 -1
  52. package/script/server/service.js +8 -0
  53. package/script/server.d.ts.map +1 -1
  54. package/script/server.js +54 -6
  55. package/script/service/deno.d.ts +1 -1
  56. package/script/service/deno.d.ts.map +1 -1
  57. package/script/service/mod.d.ts +1 -1
  58. package/script/service/mod.d.ts.map +1 -1
  59. package/script/service/node.d.ts +1 -1
  60. package/script/service/node.d.ts.map +1 -1
  61. package/script/service/outbox_inbox.d.ts.map +1 -1
  62. package/script/service/outbox_inbox.js +14 -0
  63. package/script/telemetry/core.d.ts.map +1 -1
  64. package/script/telemetry/core.js +1 -1
  65. package/script/telemetry/env.d.ts.map +1 -1
  66. package/script/telemetry/env.js +6 -1
  67. package/script/telemetry/init.d.ts +3 -0
  68. package/script/telemetry/init.d.ts.map +1 -0
  69. package/script/telemetry/init.js +10 -0
  70. package/script/telemetry/metrics.d.ts +34 -0
  71. package/script/telemetry/metrics.d.ts.map +1 -0
  72. package/script/telemetry/metrics.js +186 -0
  73. package/script/telemetry/mod.d.ts +3 -0
  74. package/script/telemetry/mod.d.ts.map +1 -1
  75. package/script/telemetry/mod.js +7 -1
  76. package/script/telemetry/runtime.d.ts +2 -0
  77. package/script/telemetry/runtime.d.ts.map +1 -0
  78. package/script/telemetry/runtime.js +137 -0
  79. package/script/telemetry.d.ts +3 -0
  80. package/script/telemetry.d.ts.map +1 -0
  81. package/script/telemetry.js +18 -0
  82. package/script/transfer.d.ts.map +1 -1
  83. package/script/transfer.js +28 -17
  84. package/script/trellis.d.ts.map +1 -1
  85. package/script/trellis.js +490 -86
  86. package/src/errors/TrellisError.ts +4 -4
  87. package/src/server/internal_jobs/job-manager.ts +35 -5
  88. package/src/server/runtime.ts +4 -0
  89. package/src/server/service.ts +27 -0
  90. package/src/server.ts +66 -11
  91. package/src/service/deno.ts +1 -0
  92. package/src/service/mod.ts +1 -0
  93. package/src/service/node.ts +1 -0
  94. package/src/service/outbox_inbox.ts +14 -0
  95. package/src/telemetry/core.ts +1 -1
  96. package/src/telemetry/env.ts +5 -1
  97. package/src/telemetry/init.ts +8 -0
  98. package/src/telemetry/metrics.ts +294 -0
  99. package/src/telemetry/mod.ts +7 -0
  100. package/src/telemetry/runtime.ts +218 -0
  101. package/src/telemetry.ts +2 -0
  102. package/src/transfer.ts +69 -30
  103. package/src/trellis.ts +487 -88
  104. package/esm/tracing.d.ts +0 -5
  105. package/esm/tracing.d.ts.map +0 -1
  106. package/esm/tracing.js +0 -8
  107. package/script/tracing.d.ts +0 -5
  108. package/script/tracing.d.ts.map +0 -1
  109. package/script/tracing.js +0 -27
  110. 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";
@@ -781,6 +783,16 @@ export function annotateHandlerBoundaryError(
781
783
  return error;
782
784
  }
783
785
 
786
+ function recordRuntimeError(
787
+ error: unknown,
788
+ attributes: TrellisErrorMetricAttributes,
789
+ ): void {
790
+ recordTrellisError(error, {
791
+ messagingSystem: "nats",
792
+ ...attributes,
793
+ });
794
+ }
795
+
784
796
  export type RuntimeOperationDesc = {
785
797
  subject: string;
786
798
  input: unknown;
@@ -2464,11 +2476,23 @@ export class Trellis<
2464
2476
 
2465
2477
  const msg = encodeRuntimeSchema(ctx.input, input).take();
2466
2478
  if (isErr(msg)) {
2479
+ recordRuntimeError(msg.error, {
2480
+ surface: "rpc",
2481
+ direction: "client",
2482
+ operation: method,
2483
+ phase: "request_encoding",
2484
+ });
2467
2485
  return msg;
2468
2486
  }
2469
2487
 
2470
2488
  const subject = this.template(ctx.subject, input).take();
2471
2489
  if (isErr(subject)) {
2490
+ recordRuntimeError(subject.error, {
2491
+ surface: "rpc",
2492
+ direction: "client",
2493
+ operation: method,
2494
+ phase: "request_encoding",
2495
+ });
2472
2496
  return subject;
2473
2497
  }
2474
2498
 
@@ -2493,13 +2517,19 @@ export class Trellis<
2493
2517
  });
2494
2518
  const response = msgResult.take();
2495
2519
  if (isErr(response)) {
2520
+ recordRuntimeError(response.error, {
2521
+ surface: "rpc",
2522
+ direction: "client",
2523
+ operation: method,
2524
+ phase: "request_send",
2525
+ });
2496
2526
  return response;
2497
2527
  }
2498
2528
 
2499
2529
  if (response.headers?.get("status") === "error") {
2500
2530
  const json = safeJson(response).take();
2501
2531
  if (isErr(json)) {
2502
- return err(requestFailedTransportError({
2532
+ const error = requestFailedTransportError({
2503
2533
  code: "trellis.request.invalid_response",
2504
2534
  message: "Trellis returned an invalid response.",
2505
2535
  hint:
@@ -2507,12 +2537,19 @@ export class Trellis<
2507
2537
  method,
2508
2538
  subject,
2509
2539
  cause: json.error.cause,
2510
- }));
2540
+ });
2541
+ recordRuntimeError(error, {
2542
+ surface: "rpc",
2543
+ direction: "client",
2544
+ operation: method,
2545
+ phase: "response_decoding",
2546
+ });
2547
+ return err(error);
2511
2548
  }
2512
2549
 
2513
2550
  const errorData = parse(TrellisErrorDataSchema, json).take();
2514
2551
  if (isErr(errorData)) {
2515
- return err(requestFailedTransportError({
2552
+ const error = requestFailedTransportError({
2516
2553
  code: "trellis.request.invalid_response",
2517
2554
  message: "Trellis returned an invalid response.",
2518
2555
  hint:
@@ -2520,7 +2557,14 @@ export class Trellis<
2520
2557
  method,
2521
2558
  subject,
2522
2559
  cause: errorData.error,
2523
- }));
2560
+ });
2561
+ recordRuntimeError(error, {
2562
+ surface: "rpc",
2563
+ direction: "client",
2564
+ operation: method,
2565
+ phase: "response_decoding",
2566
+ });
2567
+ return err(error);
2524
2568
  }
2525
2569
 
2526
2570
  const declaredErrorTypes = Array.isArray(ctx.declaredErrorTypes)
@@ -2539,17 +2583,29 @@ export class Trellis<
2539
2583
  );
2540
2584
  if (reconstructed) {
2541
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
+ });
2542
2592
  return err(reconstructed);
2543
2593
  }
2544
2594
 
2545
2595
  const remoteError = new RemoteError({ error: errorData });
2546
2596
  await this.#handleBrowserAuthRequired(remoteError);
2597
+ recordRuntimeError(remoteError, {
2598
+ surface: "rpc",
2599
+ direction: "client",
2600
+ operation: method,
2601
+ phase: "remote_error",
2602
+ });
2547
2603
  return err(remoteError);
2548
2604
  }
2549
2605
 
2550
2606
  const json = safeJson(response).take();
2551
2607
  if (isErr(json)) {
2552
- return err(requestFailedTransportError({
2608
+ const error = requestFailedTransportError({
2553
2609
  code: "trellis.request.invalid_response",
2554
2610
  message: "Trellis returned an invalid response.",
2555
2611
  hint:
@@ -2557,11 +2613,24 @@ export class Trellis<
2557
2613
  method,
2558
2614
  subject,
2559
2615
  cause: json.error.cause,
2560
- }));
2616
+ });
2617
+ recordRuntimeError(error, {
2618
+ surface: "rpc",
2619
+ direction: "client",
2620
+ operation: method,
2621
+ phase: "response_decoding",
2622
+ });
2623
+ return err(error);
2561
2624
  }
2562
2625
 
2563
2626
  const outputResult = parseRuntimeSchema(ctx.output, json).take();
2564
2627
  if (isErr(outputResult)) {
2628
+ recordRuntimeError(outputResult.error, {
2629
+ surface: "rpc",
2630
+ direction: "client",
2631
+ operation: method,
2632
+ phase: "response_decoding",
2633
+ });
2565
2634
  return err(outputResult.error);
2566
2635
  }
2567
2636
 
@@ -2590,6 +2659,12 @@ export class Trellis<
2590
2659
  message: unexpected.message,
2591
2660
  });
2592
2661
  span.recordException(unexpected);
2662
+ recordRuntimeError(unexpected, {
2663
+ surface: "rpc",
2664
+ direction: "client",
2665
+ operation: method,
2666
+ phase: "unexpected",
2667
+ });
2593
2668
  return err(unexpected);
2594
2669
  } finally {
2595
2670
  span.end();
@@ -2738,13 +2813,29 @@ export class Trellis<
2738
2813
  ): AsyncResult<FeedSubscription<TEvent>, BaseError> {
2739
2814
  return AsyncResult.from((async () => {
2740
2815
  const payload = encodeRuntimeSchema(descriptor.input, input).take();
2741
- 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
+ }
2742
2825
 
2743
2826
  const subject = this.template(
2744
2827
  descriptor.subject,
2745
2828
  input as Record<string, unknown>,
2746
2829
  ).take();
2747
- 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
+ }
2748
2839
 
2749
2840
  const authHeaders = await this.#createProof(subject, payload);
2750
2841
  const headers = natsHeaders();
@@ -2766,14 +2857,21 @@ export class Trellis<
2766
2857
  } catch (cause) {
2767
2858
  opts?.signal?.removeEventListener("abort", abort);
2768
2859
  sub.unsubscribe();
2769
- return err(createTransportError({
2860
+ const error = createTransportError({
2770
2861
  code: "trellis.feed.subscribe_failed",
2771
2862
  message: "Trellis could not subscribe to the feed.",
2772
2863
  hint:
2773
2864
  "Retry the subscription. If it keeps failing, check Trellis runtime health.",
2774
2865
  cause,
2775
2866
  context: { feed, subject },
2776
- }));
2867
+ });
2868
+ recordRuntimeError(error, {
2869
+ surface: "feed",
2870
+ direction: "client",
2871
+ operation: feed,
2872
+ phase: "request_send",
2873
+ });
2874
+ return err(error);
2777
2875
  }
2778
2876
 
2779
2877
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
@@ -2804,7 +2902,7 @@ export class Trellis<
2804
2902
  if (firstFrame === "timeout" || firstFrame === "aborted") {
2805
2903
  opts?.signal?.removeEventListener("abort", abort);
2806
2904
  sub.unsubscribe();
2807
- return err(createTransportError({
2905
+ const error = createTransportError({
2808
2906
  code: firstFrame === "timeout"
2809
2907
  ? "trellis.feed.subscribe_timeout"
2810
2908
  : "trellis.feed.subscribe_aborted",
@@ -2815,30 +2913,51 @@ export class Trellis<
2815
2913
  ? "Check that the target service is running and has the current deployment digest, then retry."
2816
2914
  : "Retry the subscription if the feed is still needed.",
2817
2915
  context: { feed, subject },
2818
- }));
2916
+ });
2917
+ recordRuntimeError(error, {
2918
+ surface: "feed",
2919
+ direction: "client",
2920
+ operation: feed,
2921
+ phase: "handshake",
2922
+ });
2923
+ return err(error);
2819
2924
  }
2820
2925
  if (firstFrame.done) {
2821
2926
  opts?.signal?.removeEventListener("abort", abort);
2822
2927
  sub.unsubscribe();
2823
- return err(createTransportError({
2928
+ const error = createTransportError({
2824
2929
  code: "trellis.feed.subscribe_closed",
2825
2930
  message: "Trellis closed the feed before acknowledging it.",
2826
2931
  hint:
2827
2932
  "Retry the subscription. If it keeps failing, check Trellis runtime health.",
2828
2933
  context: { feed, subject },
2829
- }));
2934
+ });
2935
+ recordRuntimeError(error, {
2936
+ surface: "feed",
2937
+ direction: "client",
2938
+ operation: feed,
2939
+ phase: "handshake",
2940
+ });
2941
+ return err(error);
2830
2942
  }
2831
2943
  const firstMessage = firstFrame.value;
2832
2944
  if (firstMessage.headers?.get("status") === "error") {
2833
2945
  opts?.signal?.removeEventListener("abort", abort);
2834
2946
  sub.unsubscribe();
2835
- return err(createTransportError({
2947
+ const error = createTransportError({
2836
2948
  code: "trellis.feed.failed",
2837
2949
  message: "Trellis rejected the feed subscription.",
2838
2950
  hint:
2839
2951
  "Retry the subscription. If it keeps failing, check Trellis runtime health and permissions.",
2840
2952
  context: { feed, subject, frame: firstMessage.string() },
2841
- }));
2953
+ });
2954
+ recordRuntimeError(error, {
2955
+ surface: "feed",
2956
+ direction: "client",
2957
+ operation: feed,
2958
+ phase: "remote_error",
2959
+ });
2960
+ return err(error);
2842
2961
  }
2843
2962
  const firstEvent = firstMessage.headers?.get("feed-status") === "ready"
2844
2963
  ? undefined
@@ -2849,18 +2968,41 @@ export class Trellis<
2849
2968
  try {
2850
2969
  const parseFeedFrame = (msg: Msg): TEvent => {
2851
2970
  if (msg.headers?.get("status") === "error") {
2852
- throw createTransportError({
2971
+ const error = createTransportError({
2853
2972
  code: "trellis.feed.failed",
2854
2973
  message: "Trellis stopped the feed.",
2855
2974
  hint:
2856
2975
  "Retry the subscription. If it keeps failing, check Trellis runtime health.",
2857
2976
  context: { feed, subject, frame: msg.string() },
2858
2977
  });
2978
+ recordRuntimeError(error, {
2979
+ surface: "feed",
2980
+ direction: "client",
2981
+ operation: feed,
2982
+ phase: "remote_error",
2983
+ });
2984
+ throw error;
2859
2985
  }
2860
2986
  const json = safeJson(msg).take();
2861
- 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
+ }
2862
2996
  const parsed = parseRuntimeSchema(eventSchema, json).take();
2863
- 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
+ }
2864
3006
  return parsed as TEvent;
2865
3007
  };
2866
3008
  if (firstEvent) yield parseFeedFrame(firstEvent);
@@ -2891,7 +3033,7 @@ export class Trellis<
2891
3033
  sub = this.nats.subscribe(subject);
2892
3034
  await this.nats.flush();
2893
3035
  } catch (cause) {
2894
- throw createTransportError({
3036
+ const error = createTransportError({
2895
3037
  code: "trellis.feed.listen_failed",
2896
3038
  message: "Trellis could not listen for feed requests.",
2897
3039
  hint:
@@ -2899,6 +3041,13 @@ export class Trellis<
2899
3041
  cause,
2900
3042
  context: { feed, subject },
2901
3043
  });
3044
+ recordRuntimeError(error, {
3045
+ surface: "feed",
3046
+ direction: "server",
3047
+ operation: feed,
3048
+ phase: "listen",
3049
+ });
3050
+ throw error;
2902
3051
  }
2903
3052
  const task = AsyncResult.try(async () => {
2904
3053
  for await (const msg of sub) {
@@ -2923,6 +3072,12 @@ export class Trellis<
2923
3072
  contractDigest: this.contractDigest,
2924
3073
  traceId: traceIdFromTraceparent(msg.headers?.get("traceparent")),
2925
3074
  });
3075
+ recordRuntimeError(error, {
3076
+ surface: "feed",
3077
+ direction: "server",
3078
+ operation: feed,
3079
+ phase: "handler_throw",
3080
+ });
2926
3081
  this.#respondWithError(msg, error);
2927
3082
  }
2928
3083
  })();
@@ -2940,9 +3095,25 @@ export class Trellis<
2940
3095
  ) => unknown | Promise<unknown>,
2941
3096
  ): Promise<Result<void, BaseError>> {
2942
3097
  const json = safeJson(msg).take();
2943
- 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
+ }
2944
3107
  const parsed = parseRuntimeSchema(descriptor.input, json).take();
2945
- 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
+ }
2946
3117
 
2947
3118
  const caller = await this.#authenticateFeedRequest({
2948
3119
  feed,
@@ -2952,13 +3123,26 @@ export class Trellis<
2952
3123
  requiredCapabilities: descriptor.subscribeCapabilities,
2953
3124
  });
2954
3125
  const callerValue = caller.take();
2955
- 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
+ }
2956
3135
  if (!msg.reply) {
2957
- return err(
2958
- new UnexpectedError({
2959
- context: { feed, reason: "missing_reply" },
2960
- }),
2961
- );
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);
2962
3146
  }
2963
3147
  const readyHeaders = natsHeaders();
2964
3148
  readyHeaders.set("feed-status", "ready");
@@ -2974,16 +3158,43 @@ export class Trellis<
2974
3158
  emit: (event: TEvent) =>
2975
3159
  AsyncResult.from((async () => {
2976
3160
  const payload = encodeRuntimeSchema(descriptor.event, event).take();
2977
- 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
+ }
2978
3170
  if (!msg.reply) {
2979
- return err(
2980
- new UnexpectedError({
2981
- context: { feed, reason: "missing_reply" },
2982
- }),
2983
- );
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);
2984
3197
  }
2985
- this.nats.publish(msg.reply, payload);
2986
- await this.nats.flush();
2987
3198
  return ok(undefined);
2988
3199
  })()),
2989
3200
  });
@@ -2991,14 +3202,21 @@ export class Trellis<
2991
3202
  ? handlerResult.take()
2992
3203
  : handlerResult;
2993
3204
  if (isErr(handlerOutcome)) {
2994
- return err(annotateHandlerBoundaryError(handlerOutcome.error, {
3205
+ const error = annotateHandlerBoundaryError(handlerOutcome.error, {
2995
3206
  feed,
2996
3207
  requestId: msg.headers?.get("request-id"),
2997
3208
  service: this.name,
2998
3209
  contractId: this.contractId,
2999
3210
  contractDigest: this.contractDigest,
3000
3211
  traceId: traceIdFromTraceparent(msg.headers?.get("traceparent")),
3001
- }));
3212
+ });
3213
+ recordRuntimeError(error, {
3214
+ surface: "feed",
3215
+ direction: "server",
3216
+ operation: feed,
3217
+ phase: "handler_result",
3218
+ });
3219
+ return err(error);
3002
3220
  }
3003
3221
  return ok(undefined);
3004
3222
  } finally {
@@ -3186,6 +3404,12 @@ export class Trellis<
3186
3404
  code: SpanStatusCode.ERROR,
3187
3405
  message: "Failed to parse JSON",
3188
3406
  });
3407
+ recordRuntimeError(jsonData.error, {
3408
+ surface: "rpc",
3409
+ direction: "server",
3410
+ operation: String(method),
3411
+ phase: "parse",
3412
+ });
3189
3413
  return jsonData;
3190
3414
  }
3191
3415
 
@@ -3195,6 +3419,12 @@ export class Trellis<
3195
3419
  code: SpanStatusCode.ERROR,
3196
3420
  message: "Input validation failed",
3197
3421
  });
3422
+ recordRuntimeError(parsedInput.error, {
3423
+ surface: "rpc",
3424
+ direction: "server",
3425
+ operation: String(method),
3426
+ phase: "input_validation",
3427
+ });
3198
3428
  return parsedInput;
3199
3429
  }
3200
3430
 
@@ -3221,7 +3451,14 @@ export class Trellis<
3221
3451
  code: SpanStatusCode.ERROR,
3222
3452
  message: "Missing session-key",
3223
3453
  });
3224
- 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);
3225
3462
  }
3226
3463
  if (!proof) {
3227
3464
  this.#log.warn({ method }, "Missing proof in request");
@@ -3229,11 +3466,25 @@ export class Trellis<
3229
3466
  code: SpanStatusCode.ERROR,
3230
3467
  message: "Missing proof",
3231
3468
  });
3232
- 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);
3233
3477
  }
3234
3478
  const iat = Number(iatHeader);
3235
3479
  if (!Number.isSafeInteger(iat) || !requestId) {
3236
- 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);
3237
3488
  }
3238
3489
 
3239
3490
  // Verify proof signature locally using the raw request bytes we received.
@@ -3272,12 +3523,17 @@ export class Trellis<
3272
3523
  code: SpanStatusCode.ERROR,
3273
3524
  message: "Invalid signature",
3274
3525
  });
3275
- return err(
3276
- new AuthError({
3277
- reason: "invalid_signature",
3278
- context: { sessionKey },
3279
- }),
3280
- );
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);
3281
3537
  }
3282
3538
 
3283
3539
  let auth:
@@ -3323,11 +3579,16 @@ export class Trellis<
3323
3579
  }
3324
3580
 
3325
3581
  if (!auth) {
3326
- return err(
3327
- new UnexpectedError({
3328
- context: { reason: "missing_auth_validate_result" },
3329
- }),
3330
- );
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);
3331
3592
  }
3332
3593
 
3333
3594
  if (auth instanceof Error) {
@@ -3347,9 +3608,22 @@ export class Trellis<
3347
3608
  message: "Auth.Requests.Validate failed",
3348
3609
  });
3349
3610
  if (auth instanceof BaseError) {
3611
+ recordRuntimeError(auth, {
3612
+ surface: "rpc",
3613
+ direction: "server",
3614
+ operation: String(method),
3615
+ phase: "auth",
3616
+ });
3350
3617
  return err(auth);
3351
3618
  }
3352
- 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);
3353
3627
  }
3354
3628
 
3355
3629
  if (!auth.allowed) {
@@ -3357,15 +3631,20 @@ export class Trellis<
3357
3631
  code: SpanStatusCode.ERROR,
3358
3632
  message: "Insufficient permissions",
3359
3633
  });
3360
- return err(
3361
- new AuthError({
3362
- reason: "insufficient_permissions",
3363
- context: {
3364
- requiredCapabilities: ctx.callerCapabilities,
3365
- userCapabilities: auth.caller.capabilities,
3366
- },
3367
- }),
3368
- );
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);
3369
3648
  }
3370
3649
 
3371
3650
  if (
@@ -3376,12 +3655,17 @@ export class Trellis<
3376
3655
  code: SpanStatusCode.ERROR,
3377
3656
  message: "Reply subject mismatch",
3378
3657
  });
3379
- return err(
3380
- new AuthError({
3381
- reason: "reply_subject_mismatch",
3382
- context: { expected: auth.inboxPrefix, actual: msg.reply },
3383
- }),
3384
- );
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);
3385
3669
  }
3386
3670
 
3387
3671
  caller = auth.caller;
@@ -3449,6 +3733,12 @@ export class Trellis<
3449
3733
  message: error.message,
3450
3734
  });
3451
3735
  span.recordException(error);
3736
+ recordRuntimeError(error, {
3737
+ surface: "rpc",
3738
+ direction: "server",
3739
+ operation: String(method),
3740
+ phase: "handler_throw",
3741
+ });
3452
3742
  return err(error);
3453
3743
  }
3454
3744
 
@@ -3481,6 +3771,12 @@ export class Trellis<
3481
3771
  code: SpanStatusCode.ERROR,
3482
3772
  message: error.message,
3483
3773
  });
3774
+ recordRuntimeError(error, {
3775
+ surface: "rpc",
3776
+ direction: "server",
3777
+ operation: String(method),
3778
+ phase: "handler_result",
3779
+ });
3484
3780
  return err(error);
3485
3781
  }
3486
3782
 
@@ -3490,6 +3786,12 @@ export class Trellis<
3490
3786
  code: SpanStatusCode.ERROR,
3491
3787
  message: "Output encoding failed",
3492
3788
  });
3789
+ recordRuntimeError(encoded.error, {
3790
+ surface: "rpc",
3791
+ direction: "server",
3792
+ operation: String(method),
3793
+ phase: "output_encoding",
3794
+ });
3493
3795
  return encoded;
3494
3796
  }
3495
3797
 
@@ -3614,17 +3916,28 @@ export class Trellis<
3614
3916
  typeof eventName
3615
3917
  >;
3616
3918
  if (!ctx) {
3617
- return err(
3618
- new UnexpectedError({
3619
- cause: this.#unknownApiError("event", event.toString()),
3620
- context: { event: event.toString() },
3621
- }),
3622
- );
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);
3623
3930
  }
3624
3931
 
3625
3932
  const subject = this.template(ctx.subject, data).take();
3626
3933
  if (isErr(subject)) {
3627
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
+ });
3628
3941
  return subject;
3629
3942
  }
3630
3943
 
@@ -3639,7 +3952,14 @@ export class Trellis<
3639
3952
  const msg = encodeSchema(ctx.event, payload).take();
3640
3953
  if (isErr(msg)) {
3641
3954
  logger.error({ err: msg.error }, "Failed to encode event.");
3642
- 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);
3643
3963
  }
3644
3964
 
3645
3965
  const headers = natsHeaders();
@@ -3659,9 +3979,17 @@ export class Trellis<
3659
3979
  headers: Object.freeze(headerRecord),
3660
3980
  }));
3661
3981
  } catch (cause) {
3662
- return err(
3663
- new UnexpectedError({ cause, context: { event: event.toString() } }),
3664
- );
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);
3665
3993
  }
3666
3994
  }
3667
3995
 
@@ -3686,9 +4014,17 @@ export class Trellis<
3686
4014
  await this.js.publish(event.subject, event.encodedPayload, { headers });
3687
4015
  return ok(undefined);
3688
4016
  } catch (cause) {
3689
- return err(
3690
- new UnexpectedError({ cause, context: { event: event.event } }),
3691
- );
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);
3692
4028
  }
3693
4029
  })());
3694
4030
  }
@@ -3795,6 +4131,12 @@ export class Trellis<
3795
4131
  const m = parsedEvent.take();
3796
4132
  if (isErr(m)) {
3797
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
+ });
3798
4140
  continue;
3799
4141
  }
3800
4142
 
@@ -3807,6 +4149,12 @@ export class Trellis<
3807
4149
  });
3808
4150
  const handlerValue = handlerResult.take();
3809
4151
  if (isErr(handlerValue)) {
4152
+ recordRuntimeError(handlerValue.error, {
4153
+ surface: "event",
4154
+ direction: "consumer",
4155
+ operation: String(event),
4156
+ phase: "handler_result",
4157
+ });
3810
4158
  this.#log.error(
3811
4159
  {
3812
4160
  error: handlerValue.error.toSerializable(),
@@ -4124,6 +4472,12 @@ export class Trellis<
4124
4472
  { error: eventPayload.error },
4125
4473
  "Event validation failed",
4126
4474
  );
4475
+ recordRuntimeError(eventPayload.error, {
4476
+ surface: "event",
4477
+ direction: "consumer",
4478
+ operation: String(registration.event),
4479
+ phase: "input_validation",
4480
+ });
4127
4481
  msg.term();
4128
4482
  failed = true;
4129
4483
  break;
@@ -4139,6 +4493,12 @@ export class Trellis<
4139
4493
  });
4140
4494
  const handlerValue = handlerResult.take();
4141
4495
  if (isErr(handlerValue)) {
4496
+ recordRuntimeError(handlerValue.error, {
4497
+ surface: "event",
4498
+ direction: "consumer",
4499
+ operation: String(registration.event),
4500
+ phase: "handler_result",
4501
+ });
4142
4502
  this.#log.error(
4143
4503
  {
4144
4504
  error: handlerValue.error.toSerializable(),
@@ -4348,6 +4708,12 @@ export class Trellis<
4348
4708
  code: SpanStatusCode.ERROR,
4349
4709
  message: response.error.message,
4350
4710
  });
4711
+ recordRuntimeError(response.error, {
4712
+ surface: "operation",
4713
+ direction: "client",
4714
+ operation: "requestJson",
4715
+ phase: "request_send",
4716
+ });
4351
4717
  return response;
4352
4718
  }
4353
4719
 
@@ -4365,6 +4731,12 @@ export class Trellis<
4365
4731
  code: SpanStatusCode.ERROR,
4366
4732
  message: error.message,
4367
4733
  });
4734
+ recordRuntimeError(error, {
4735
+ surface: "operation",
4736
+ direction: "client",
4737
+ operation: "requestJson",
4738
+ phase: "response_decoding",
4739
+ });
4368
4740
  return err(error);
4369
4741
  }
4370
4742
 
@@ -4377,6 +4749,12 @@ export class Trellis<
4377
4749
  message: error.message,
4378
4750
  });
4379
4751
  span.recordException(error);
4752
+ recordRuntimeError(error, {
4753
+ surface: "operation",
4754
+ direction: "client",
4755
+ operation: "requestJson",
4756
+ phase: "unexpected",
4757
+ });
4380
4758
  return err(error);
4381
4759
  } finally {
4382
4760
  span.end();
@@ -4413,40 +4791,61 @@ export class Trellis<
4413
4791
  await this.nats.flush();
4414
4792
  } catch (cause) {
4415
4793
  sub.unsubscribe();
4416
- return err(createTransportError({
4794
+ const error = createTransportError({
4417
4795
  code: "trellis.watch.failed",
4418
4796
  message: "Trellis could not start the operation watch.",
4419
4797
  hint:
4420
4798
  "Retry watching the operation. If it keeps failing, reconnect to Trellis and try again.",
4421
4799
  cause,
4422
4800
  context: { subject },
4423
- }));
4801
+ });
4802
+ recordRuntimeError(error, {
4803
+ surface: "operation",
4804
+ direction: "client",
4805
+ operation: "watchJson",
4806
+ phase: "request_send",
4807
+ });
4808
+ return err(error);
4424
4809
  }
4425
4810
 
4426
4811
  return ok((async function* () {
4427
4812
  try {
4428
4813
  for await (const msg of sub) {
4429
4814
  if (msg.headers?.get("status") === "error") {
4430
- yield err(createTransportError({
4815
+ const error = createTransportError({
4431
4816
  code: "trellis.watch.failed",
4432
4817
  message: "Trellis stopped the operation watch.",
4433
4818
  hint:
4434
4819
  "Retry watching the operation. If it keeps happening, reconnect to Trellis and try again.",
4435
4820
  context: { subject, frame: msg.string() },
4436
- }));
4821
+ });
4822
+ recordRuntimeError(error, {
4823
+ surface: "operation",
4824
+ direction: "client",
4825
+ operation: "watchJson",
4826
+ phase: "remote_error",
4827
+ });
4828
+ yield err(error);
4437
4829
  continue;
4438
4830
  }
4439
4831
 
4440
4832
  const json = safeJson(msg).take();
4441
4833
  if (isErr(json)) {
4442
- yield err(createTransportError({
4834
+ const error = createTransportError({
4443
4835
  code: "trellis.watch.invalid_response",
4444
4836
  message: "Trellis returned an invalid watch update.",
4445
4837
  hint:
4446
4838
  "Retry watching the operation. If it keeps happening, reconnect to Trellis and try again.",
4447
4839
  cause: json.error.cause,
4448
4840
  context: { subject },
4449
- }));
4841
+ });
4842
+ recordRuntimeError(error, {
4843
+ surface: "operation",
4844
+ direction: "client",
4845
+ operation: "watchJson",
4846
+ phase: "response_decoding",
4847
+ });
4848
+ yield err(error);
4450
4849
  continue;
4451
4850
  }
4452
4851