authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-05 16:35:14-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-05 16:35:14-07:00
loga56496c7f20d53369ef87101681ee2a99b3a4337
treeab5c0f6d14af5fcadeba0246578336b94c18855a
parentb76f161d551235e1de89ed85c747f14041ac5d99
signature Signed by PGP key B5690EEEBB952194

Annotate relay error spans with schema fields (#2976)


20 files changed, 744 insertions(+), 232 deletions(-)

infra/relay/src/agentActivity/AgentActivityRows.ts+21-13
......@@ -1,7 +1,6 @@
11import type { RelayAgentActivityState } from "@t3tools/contracts/relay";
22import { RelayAgentActivityState as RelayAgentActivityStateSchema } from "@t3tools/contracts/relay";
33import * as Context from "effect/Context";
4import * as Data from "effect/Data";
54import * as DateTime from "effect/DateTime";
65import * as Effect from "effect/Effect";
76import { cast } from "effect/Function";
......@@ -13,23 +12,32 @@ import { and, desc, eq, isNull } from "drizzle-orm";
1312import { RelayDb } from "../db.ts";
1413import { relayAgentActivityRows, relayEnvironmentLinks } from "../persistence/schema.ts";
1514
16export class AgentActivityRowUpsertPersistenceError extends Data.TaggedError(
15export class AgentActivityRowUpsertPersistenceError extends Schema.TaggedErrorClass<AgentActivityRowUpsertPersistenceError>()(
1716 "AgentActivityRowUpsertPersistenceError",
18)<{
19 readonly cause: unknown;
20}> {}
17 { cause: Schema.Defect() },
18) {
19 override get message(): string {
20 return "Failed to persist agent activity state";
21 }
22}
2123
22export class AgentActivityRowDeletePersistenceError extends Data.TaggedError(
24export class AgentActivityRowDeletePersistenceError extends Schema.TaggedErrorClass<AgentActivityRowDeletePersistenceError>()(
2325 "AgentActivityRowDeletePersistenceError",
24)<{
25 readonly cause: unknown;
26}> {}
26 { cause: Schema.Defect() },
27) {
28 override get message(): string {
29 return "Failed to delete agent activity state";
30 }
31}
2732
28export class AgentActivityRowListPersistenceError extends Data.TaggedError(
33export class AgentActivityRowListPersistenceError extends Schema.TaggedErrorClass<AgentActivityRowListPersistenceError>()(
2934 "AgentActivityRowListPersistenceError",
30)<{
31 readonly cause: unknown;
32}> {}
35 { cause: Schema.Defect() },
36) {
37 override get message(): string {
38 return "Failed to list agent activity state";
39 }
40}
3341
3442export interface AgentActivityRowsShape {
3543 readonly upsert: (input: {
infra/relay/src/agentActivity/ApnsClient.ts+32-18
......@@ -2,20 +2,13 @@ import * as NodeCrypto from "node:crypto";
22
33import type { RelayAgentActivityAggregateState } from "@t3tools/contracts/relay";
44import * as Context from "effect/Context";
5import * as Data from "effect/Data";
65import * as Effect from "effect/Effect";
76import * as Encoding from "effect/Encoding";
87import * as Layer from "effect/Layer";
98import * as Option from "effect/Option";
109import * as Redacted from "effect/Redacted";
1110import * as Schema from "effect/Schema";
12import {
13 Headers,
14 HttpClient,
15 HttpClientRequest,
16 type HttpBody,
17 type HttpClientError,
18} from "effect/unstable/http";
11import { Headers, HttpClient, HttpClientRequest } from "effect/unstable/http";
1912import type { ApnsCredentials } from "../Config.ts";
2013import type { ApnsNotificationPayload } from "./apnsDeliveryJobs.ts";
2114
......@@ -45,18 +38,39 @@ export interface ApnsDeliveryResult {
4538 readonly apnsId: string | null;
4639}
4740
48export class ApnsSigningError extends Data.TaggedError("ApnsSigningError")<{
49 readonly phase: "encoding" | "signing";
50 readonly cause: unknown;
51}> {}
41export class ApnsSigningError extends Schema.TaggedErrorClass<ApnsSigningError>()(
42 "ApnsSigningError",
43 {
44 phase: Schema.Literals(["encoding", "signing"]),
45 cause: Schema.Defect(),
46 },
47) {
48 override get message(): string {
49 return `Failed during APNs JWT ${this.phase}`;
50 }
51}
5252
53export class ApnsHttpRequestError extends Data.TaggedError("ApnsHttpRequestError")<{
54 readonly cause: HttpClientError.HttpClientError | HttpBody.HttpBodyError;
55}> {}
53export class ApnsHttpRequestError extends Schema.TaggedErrorClass<ApnsHttpRequestError>()(
54 "ApnsHttpRequestError",
55 {
56 cause: Schema.Defect(),
57 },
58) {
59 override get message(): string {
60 return "APNs HTTP request failed";
61 }
62}
5663
57export class ApnsInvalidResponseError extends Data.TaggedError("ApnsInvalidResponseError")<{
58 readonly cause: unknown;
59}> {}
64export class ApnsInvalidResponseError extends Schema.TaggedErrorClass<ApnsInvalidResponseError>()(
65 "ApnsInvalidResponseError",
66 {
67 cause: Schema.Defect(),
68 },
69) {
70 override get message(): string {
71 return "APNs returned an invalid response";
72 }
73}
6074
6175export type ApnsError = ApnsSigningError | ApnsHttpRequestError | ApnsInvalidResponseError;
6276
infra/relay/src/agentActivity/ApnsDeliveries.ts+10-4
......@@ -9,7 +9,6 @@ import {
99 RelayAgentAwarenessPreferences as RelayAgentAwarenessPreferencesSchema,
1010} from "@t3tools/contracts/relay";
1111import * as Context from "effect/Context";
12import * as Data from "effect/Data";
1312import * as DateTime from "effect/DateTime";
1413import * as Effect from "effect/Effect";
1514import * as Layer from "effect/Layer";
......@@ -74,9 +73,16 @@ export type ApnsDeliveryError =
7473 | LiveActivities.LiveActivityTargetListPersistenceError
7574 | LiveActivities.LiveActivityDeliveryMarkPersistenceError;
7675
77export class ApnsDeliveryJobClaimInFlight extends Data.TaggedError("ApnsDeliveryJobClaimInFlight")<{
78 readonly sourceJobId: string;
79}> {}
76export class ApnsDeliveryJobClaimInFlight extends Schema.TaggedErrorClass<ApnsDeliveryJobClaimInFlight>()(
77 "ApnsDeliveryJobClaimInFlight",
78 {
79 sourceJobId: Schema.String,
80 },
81) {
82 override get message(): string {
83 return `APNs delivery job '${this.sourceJobId}' is already in flight`;
84 }
85}
8086
8187const decodeRelayAgentActivityAggregateStateJson = Schema.decodeUnknownOption(
8288 Schema.fromJsonString(RelayAgentActivityAggregateStateSchema),
infra/relay/src/agentActivity/ApnsDeliveryQueue.ts+9-4
......@@ -2,10 +2,10 @@ import * as Alchemy from "alchemy";
22import * as Cloudflare from "alchemy/Cloudflare";
33import * as Crypto from "effect/Crypto";
44import * as Context from "effect/Context";
5import * as Data from "effect/Data";
65import * as DateTime from "effect/DateTime";
76import * as Effect from "effect/Effect";
87import * as Layer from "effect/Layer";
8import * as Schema from "effect/Schema";
99
1010import type { RelayDeliveryResult } from "@t3tools/contracts/relay";
1111
......@@ -22,9 +22,14 @@ import {
2222} from "./apnsDeliveryJobs.ts";
2323import * as RelayConfiguration from "../Config.ts";
2424
25export class ApnsDeliveryQueueSendError extends Data.TaggedError("ApnsDeliveryQueueSendError")<{
26 readonly cause: unknown;
27}> {}
25export class ApnsDeliveryQueueSendError extends Schema.TaggedErrorClass<ApnsDeliveryQueueSendError>()(
26 "ApnsDeliveryQueueSendError",
27 { cause: Schema.Defect() },
28) {
29 override get message(): string {
30 return "Failed to enqueue APNs delivery";
31 }
32}
2833
2934export type ApnsDeliveryQueueError = ApnsDeliveryQueueSendError;
3035
infra/relay/src/agentActivity/DeliveryAttempts.ts+8-5
......@@ -1,20 +1,23 @@
11import * as Context from "effect/Context";
2import * as Data from "effect/Data";
32import * as DateTime from "effect/DateTime";
43import * as Effect from "effect/Effect";
54import * as Layer from "effect/Layer";
65import * as Option from "effect/Option";
76import { and, eq, isNull } from "drizzle-orm";
87import * as Crypto from "effect/Crypto";
8import * as Schema from "effect/Schema";
99
1010import { RelayDb } from "../db.ts";
1111import { relayDeliveryAttempts } from "../persistence/schema.ts";
1212
13export class DeliveryAttemptRecordPersistenceError extends Data.TaggedError(
13export class DeliveryAttemptRecordPersistenceError extends Schema.TaggedErrorClass<DeliveryAttemptRecordPersistenceError>()(
1414 "DeliveryAttemptRecordPersistenceError",
15)<{
16 readonly cause: unknown;
17}> {}
15 { cause: Schema.Defect() },
16) {
17 override get message(): string {
18 return "Failed to persist APNs delivery attempt";
19 }
20}
1821
1922export interface DeliveryAttemptInput {
2023 readonly userId: string | null;
infra/relay/src/agentActivity/Devices.ts+23-12
......@@ -3,31 +3,42 @@ import type {
33 RelayDeviceRegistrationRequest,
44} from "@t3tools/contracts/relay";
55import * as Context from "effect/Context";
6import * as Data from "effect/Data";
76import * as DateTime from "effect/DateTime";
87import * as Effect from "effect/Effect";
98import * as Layer from "effect/Layer";
9import * as Schema from "effect/Schema";
1010import { and, eq } from "drizzle-orm";
1111import { sql } from "drizzle-orm";
1212
1313import { RelayDb } from "../db.ts";
1414import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";
1515
16export class DeviceRegistrationPersistenceError extends Data.TaggedError(
16export class DeviceRegistrationPersistenceError extends Schema.TaggedErrorClass<DeviceRegistrationPersistenceError>()(
1717 "DeviceRegistrationPersistenceError",
18)<{
19 readonly cause: unknown;
20}> {}
18 { cause: Schema.Defect() },
19) {
20 override get message(): string {
21 return "Failed to persist mobile device registration";
22 }
23}
2124
22export class DeviceUnregistrationPersistenceError extends Data.TaggedError(
25export class DeviceUnregistrationPersistenceError extends Schema.TaggedErrorClass<DeviceUnregistrationPersistenceError>()(
2326 "DeviceUnregistrationPersistenceError",
24)<{
25 readonly cause: unknown;
26}> {}
27 { cause: Schema.Defect() },
28) {
29 override get message(): string {
30 return "Failed to unregister mobile device";
31 }
32}
2733
28export class DeviceListPersistenceError extends Data.TaggedError("DeviceListPersistenceError")<{
29 readonly cause: unknown;
30}> {}
34export class DeviceListPersistenceError extends Schema.TaggedErrorClass<DeviceListPersistenceError>()(
35 "DeviceListPersistenceError",
36 { cause: Schema.Defect() },
37) {
38 override get message(): string {
39 return "Failed to list mobile devices";
40 }
41}
3142
3243export interface DevicesShape {
3344 readonly register: (input: {
infra/relay/src/agentActivity/LiveActivities.ts+21-13
......@@ -5,7 +5,6 @@ import type {
55} from "@t3tools/contracts/relay";
66import { RelayAgentActivityAggregateState as RelayAgentActivityAggregateStateSchema } from "@t3tools/contracts/relay";
77import * as Context from "effect/Context";
8import * as Data from "effect/Data";
98import * as DateTime from "effect/DateTime";
109import * as Effect from "effect/Effect";
1110import { cast } from "effect/Function";
......@@ -16,23 +15,32 @@ import { and, eq, sql } from "drizzle-orm";
1615import { RelayDb } from "../db.ts";
1716import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";
1817
19export class LiveActivityRegistrationPersistenceError extends Data.TaggedError(
18export class LiveActivityRegistrationPersistenceError extends Schema.TaggedErrorClass<LiveActivityRegistrationPersistenceError>()(
2019 "LiveActivityRegistrationPersistenceError",
21)<{
22 readonly cause: unknown;
23}> {}
20 { cause: Schema.Defect() },
21) {
22 override get message(): string {
23 return "Failed to persist Live Activity registration";
24 }
25}
2426
25export class LiveActivityTargetListPersistenceError extends Data.TaggedError(
27export class LiveActivityTargetListPersistenceError extends Schema.TaggedErrorClass<LiveActivityTargetListPersistenceError>()(
2628 "LiveActivityTargetListPersistenceError",
27)<{
28 readonly cause: unknown;
29}> {}
29 { cause: Schema.Defect() },
30) {
31 override get message(): string {
32 return "Failed to list Live Activity delivery targets";
33 }
34}
3035
31export class LiveActivityDeliveryMarkPersistenceError extends Data.TaggedError(
36export class LiveActivityDeliveryMarkPersistenceError extends Schema.TaggedErrorClass<LiveActivityDeliveryMarkPersistenceError>()(
3237 "LiveActivityDeliveryMarkPersistenceError",
33)<{
34 readonly cause: unknown;
35}> {}
38 { cause: Schema.Defect() },
39) {
40 override get message(): string {
41 return "Failed to persist Live Activity delivery state";
42 }
43}
3644
3745export interface DeviceRow {
3846 readonly user_id: string;
infra/relay/src/agentActivity/apnsDeliveryJobs.ts+17-8
......@@ -2,7 +2,6 @@ import * as NodeCrypto from "node:crypto";
22
33import { RelayAgentActivityAggregateState, type RelayDeliveryKind } from "@t3tools/contracts/relay";
44import { stableStringify } from "@t3tools/shared/relaySigning";
5import * as Data from "effect/Data";
65import * as DateTime from "effect/DateTime";
76import * as Option from "effect/Option";
87import * as Redacted from "effect/Redacted";
......@@ -50,13 +49,23 @@ export const SignedApnsDeliveryJob = Schema.Struct({
5049});
5150export type SignedApnsDeliveryJob = typeof SignedApnsDeliveryJob.Type;
5251
53export class ApnsDeliveryJobInvalid extends Data.TaggedError("ApnsDeliveryJobInvalid")<{
54 readonly message: string;
55}> {}
56
57export class ApnsDeliveryJobExpired extends Data.TaggedError("ApnsDeliveryJobExpired")<{
58 readonly expiresAt: string;
59}> {}
52export class ApnsDeliveryJobInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobInvalid>()(
53 "ApnsDeliveryJobInvalid",
54 {
55 message: Schema.String,
56 },
57) {}
58
59export class ApnsDeliveryJobExpired extends Schema.TaggedErrorClass<ApnsDeliveryJobExpired>()(
60 "ApnsDeliveryJobExpired",
61 {
62 expiresAt: Schema.String,
63 },
64) {
65 override get message(): string {
66 return `APNs delivery job expired at ${this.expiresAt}`;
67 }
68}
6069
6170export type ApnsDeliveryJobVerificationError = ApnsDeliveryJobInvalid | ApnsDeliveryJobExpired;
6271
infra/relay/src/auth/DpopProofs.ts+10-5
......@@ -1,8 +1,8 @@
11import * as Context from "effect/Context";
2import * as Data from "effect/Data";
32import * as DateTime from "effect/DateTime";
43import * as Effect from "effect/Effect";
54import * as Layer from "effect/Layer";
5import * as Schema from "effect/Schema";
66import * as HttpApiError from "effect/unstable/httpapi/HttpApiError";
77import { lt } from "drizzle-orm";
88
......@@ -10,11 +10,16 @@ import { verifyDpopProof } from "@t3tools/shared/dpop";
1010import { RelayDb } from "../db.ts";
1111import { relayDpopProofs } from "../persistence/schema.ts";
1212
13export class DpopProofReplayPersistenceError extends Data.TaggedError(
13export class DpopProofReplayPersistenceError extends Schema.TaggedErrorClass<DpopProofReplayPersistenceError>()(
1414 "DpopProofReplayPersistenceError",
15)<{
16 readonly cause: unknown;
17}> {}
15 {
16 cause: Schema.Defect(),
17 },
18) {
19 override get message(): string {
20 return "Failed to persist DPoP proof replay state";
21 }
22}
1823
1924export interface DpopProofReplayShape {
2025 readonly verifyAndConsume: (input: {
infra/relay/src/environments/EnvironmentConnector.ts+109-26
......@@ -28,7 +28,6 @@ import {
2828import { stableStringify } from "@t3tools/shared/relaySigning";
2929import * as Context from "effect/Context";
3030import * as Crypto from "effect/Crypto";
31import * as Data from "effect/Data";
3231import * as DateTime from "effect/DateTime";
3332import * as Duration from "effect/Duration";
3433import * as Effect from "effect/Effect";
......@@ -42,28 +41,53 @@ import * as EnvironmentLinks from "./EnvironmentLinks.ts";
4241import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";
4342import * as RelayConfiguration from "../Config.ts";
4443
45export class EnvironmentConnectNotAuthorized extends Data.TaggedError(
44export class EnvironmentConnectNotAuthorized extends Schema.TaggedErrorClass<EnvironmentConnectNotAuthorized>()(
4645 "EnvironmentConnectNotAuthorized",
47)<{
48 readonly environmentId: string;
49}> {}
46 {
47 environmentId: Schema.String,
48 },
49) {
50 override get message(): string {
51 return `Environment '${this.environmentId}' is not authorized to connect`;
52 }
53}
5054
51export class EnvironmentMintRequestFailed extends Data.TaggedError("EnvironmentMintRequestFailed")<{
52 readonly cause: unknown;
53}> {}
55export class EnvironmentMintRequestFailed extends Schema.TaggedErrorClass<EnvironmentMintRequestFailed>()(
56 "EnvironmentMintRequestFailed",
57 {
58 environmentId: Schema.String,
59 operation: Schema.Literals(["connect", "status"]),
60 cause: Schema.Defect(),
61 },
62) {
63 override get message(): string {
64 return `Environment '${this.environmentId}' ${this.operation} request failed`;
65 }
66}
5467
55export class EnvironmentMintRequestTimedOut extends Data.TaggedError(
68export class EnvironmentMintRequestTimedOut extends Schema.TaggedErrorClass<EnvironmentMintRequestTimedOut>()(
5669 "EnvironmentMintRequestTimedOut",
57)<{
58 readonly environmentId: string;
59 readonly timeoutMs: number;
60}> {}
70 {
71 environmentId: Schema.String,
72 timeoutMs: Schema.Number,
73 },
74) {
75 override get message(): string {
76 return `Environment '${this.environmentId}' mint request timed out after ${this.timeoutMs}ms`;
77 }
78}
6179
62export class EnvironmentMintResponseInvalid extends Data.TaggedError(
80export class EnvironmentMintResponseInvalid extends Schema.TaggedErrorClass<EnvironmentMintResponseInvalid>()(
6381 "EnvironmentMintResponseInvalid",
64)<{
65 readonly environmentId: string;
66}> {}
82 {
83 environmentId: Schema.String,
84 operation: Schema.Literals(["connect", "status"]),
85 },
86) {
87 override get message(): string {
88 return `Environment '${this.environmentId}' returned an invalid ${this.operation} response`;
89 }
90}
6791
6892export type EnvironmentConnectorError =
6993 | EnvironmentConnectNotAuthorized
......@@ -277,14 +301,28 @@ const make = Effect.gen(function* () {
277301 const now = yield* DateTime.now;
278302 const expiresAt = DateTime.add(now, { minutes: 2 });
279303 const nonce = yield* crypto.randomUUIDv4.pipe(
280 Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })),
304 Effect.mapError(
305 (cause) =>
306 new EnvironmentMintRequestFailed({
307 environmentId: input.environmentId,
308 operation: "status",
309 cause,
310 }),
311 ),
281312 );
282313 const payload = {
283314 iss: relayIssuer,
284315 aud: `t3-env:${link.environmentId}`,
285316 sub: input.userId,
286317 jti: yield* crypto.randomUUIDv4.pipe(
287 Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })),
318 Effect.mapError(
319 (cause) =>
320 new EnvironmentMintRequestFailed({
321 environmentId: input.environmentId,
322 operation: "status",
323 cause,
324 }),
325 ),
288326 ),
289327 iat: Math.floor(now.epochMilliseconds / 1_000),
290328 exp: Math.floor(expiresAt.epochMilliseconds / 1_000),
......@@ -296,7 +334,16 @@ const make = Effect.gen(function* () {
296334 privateKey: Redacted.value(settings.cloudMintPrivateKey),
297335 typ: RELAY_HEALTH_REQUEST_TYP,
298336 payload,
299 }).pipe(Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })));
337 }).pipe(
338 Effect.mapError(
339 (cause) =>
340 new EnvironmentMintRequestFailed({
341 environmentId: input.environmentId,
342 operation: "status",
343 cause,
344 }),
345 ),
346 );
300347 const checkedAt = DateTime.formatIso(now);
301348 const environmentClient = yield* makeEnvironmentClient(endpoint.httpBaseUrl);
302349 const responseOption = yield* environmentClient.cloud.health({ payload: { proof } }).pipe(
......@@ -336,7 +383,10 @@ const make = Effect.gen(function* () {
336383 now: yield* DateTime.now,
337384 });
338385 if (!verified) {
339 return yield* new EnvironmentMintResponseInvalid({ environmentId: input.environmentId });
386 return yield* new EnvironmentMintResponseInvalid({
387 environmentId: input.environmentId,
388 operation: "status",
389 });
340390 }
341391 return {
342392 environmentId: link.environmentId,
......@@ -364,14 +414,28 @@ const make = Effect.gen(function* () {
364414 const now = yield* DateTime.now;
365415 const expiresAt = DateTime.add(now, { minutes: 2 });
366416 const nonce = yield* crypto.randomUUIDv4.pipe(
367 Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })),
417 Effect.mapError(
418 (cause) =>
419 new EnvironmentMintRequestFailed({
420 environmentId: input.environmentId,
421 operation: "connect",
422 cause,
423 }),
424 ),
368425 );
369426 const payload = {
370427 iss: relayIssuer,
371428 aud: `t3-env:${link.environmentId}`,
372429 sub: input.userId,
373430 jti: yield* crypto.randomUUIDv4.pipe(
374 Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })),
431 Effect.mapError(
432 (cause) =>
433 new EnvironmentMintRequestFailed({
434 environmentId: input.environmentId,
435 operation: "connect",
436 cause,
437 }),
438 ),
375439 ),
376440 iat: Math.floor(now.epochMilliseconds / 1_000),
377441 exp: Math.floor(expiresAt.epochMilliseconds / 1_000),
......@@ -386,11 +450,27 @@ const make = Effect.gen(function* () {
386450 privateKey: Redacted.value(settings.cloudMintPrivateKey),
387451 typ: RELAY_MINT_REQUEST_TYP,
388452 payload,
389 }).pipe(Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })));
453 }).pipe(
454 Effect.mapError(
455 (cause) =>
456 new EnvironmentMintRequestFailed({
457 environmentId: input.environmentId,
458 operation: "connect",
459 cause,
460 }),
461 ),
462 );
390463 const environmentClient = yield* makeEnvironmentClient(endpoint.httpBaseUrl);
391464 const decoded = yield* environmentClient.cloud.t3MintCredential({ payload: { proof } }).pipe(
392465 withoutRedirects,
393 Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })),
466 Effect.mapError(
467 (cause) =>
468 new EnvironmentMintRequestFailed({
469 environmentId: input.environmentId,
470 operation: "connect",
471 cause,
472 }),
473 ),
394474 Effect.timeoutOption(Duration.millis(ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS)),
395475 Effect.flatMap(
396476 Option.match({
......@@ -415,7 +495,10 @@ const make = Effect.gen(function* () {
415495 nowEpochSeconds: Math.floor(now.epochMilliseconds / 1_000),
416496 });
417497 if (!verified) {
418 return yield* new EnvironmentMintResponseInvalid({ environmentId: input.environmentId });
498 return yield* new EnvironmentMintResponseInvalid({
499 environmentId: input.environmentId,
500 operation: "connect",
501 });
419502 }
420503 return {
421504 environmentId: link.environmentId,
infra/relay/src/environments/EnvironmentCredentials.ts+22-13
......@@ -1,33 +1,42 @@
11import * as Context from "effect/Context";
22import * as Crypto from "effect/Crypto";
3import * as Data from "effect/Data";
43import * as DateTime from "effect/DateTime";
54import * as Effect from "effect/Effect";
65import * as Encoding from "effect/Encoding";
76import * as Layer from "effect/Layer";
87import * as Option from "effect/Option";
8import * as Schema from "effect/Schema";
99import { and, eq, isNull, ne, notExists } from "drizzle-orm";
1010
1111import { RelayDb } from "../db.ts";
1212import { relayEnvironmentCredentials, relayEnvironmentLinks } from "../persistence/schema.ts";
1313
14export class EnvironmentCredentialCreatePersistenceError extends Data.TaggedError(
14export class EnvironmentCredentialCreatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialCreatePersistenceError>()(
1515 "EnvironmentCredentialCreatePersistenceError",
16)<{
17 readonly cause: unknown;
18}> {}
16 { cause: Schema.Defect() },
17) {
18 override get message(): string {
19 return "Failed to persist environment credential";
20 }
21}
1922
20export class EnvironmentCredentialAuthenticatePersistenceError extends Data.TaggedError(
23export class EnvironmentCredentialAuthenticatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialAuthenticatePersistenceError>()(
2124 "EnvironmentCredentialAuthenticatePersistenceError",
22)<{
23 readonly cause: unknown;
24}> {}
25 { cause: Schema.Defect() },
26) {
27 override get message(): string {
28 return "Failed to authenticate environment credential";
29 }
30}
2531
26export class EnvironmentCredentialRevokePersistenceError extends Data.TaggedError(
32export class EnvironmentCredentialRevokePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialRevokePersistenceError>()(
2733 "EnvironmentCredentialRevokePersistenceError",
28)<{
29 readonly cause: unknown;
30}> {}
34 { cause: Schema.Defect() },
35) {
36 override get message(): string {
37 return "Failed to revoke environment credential";
38 }
39}
3140
3241export interface EnvironmentCredentialPrincipal {
3342 readonly credentialId: string;
infra/relay/src/environments/EnvironmentLinker.ts+22-9
......@@ -1,6 +1,6 @@
11import {
22 RelayEnvironmentLinkProofPayload,
3 type RelayEnvironmentLinkProofInvalidReason,
3 RelayEnvironmentLinkProofInvalidReason,
44 type RelayEnvironmentLinkRequest,
55} from "@t3tools/contracts/relay";
66import {
......@@ -10,7 +10,6 @@ import {
1010 verifyRelayJwt,
1111} from "@t3tools/shared/relayJwt";
1212import * as Context from "effect/Context";
13import * as Data from "effect/Data";
1413import * as DateTime from "effect/DateTime";
1514import * as Effect from "effect/Effect";
1615import * as Layer from "effect/Layer";
......@@ -23,14 +22,28 @@ import * as EnvironmentLinks from "./EnvironmentLinks.ts";
2322import * as ManagedEndpointProvider from "./ManagedEndpointProvider.ts";
2423import * as RelayConfiguration from "../Config.ts";
2524
26export class EnvironmentLinkProofExpired extends Data.TaggedError("EnvironmentLinkProofExpired")<{
27 readonly expiresAt: string;
28}> {}
25export class EnvironmentLinkProofExpired extends Schema.TaggedErrorClass<EnvironmentLinkProofExpired>()(
26 "EnvironmentLinkProofExpired",
27 {
28 expiresAt: Schema.String,
29 },
30) {
31 override get message(): string {
32 return `Environment link proof expired at ${this.expiresAt}`;
33 }
34}
2935
30export class EnvironmentLinkProofInvalid extends Data.TaggedError("EnvironmentLinkProofInvalid")<{
31 readonly environmentId: string;
32 readonly reason: RelayEnvironmentLinkProofInvalidReason;
33}> {}
36export class EnvironmentLinkProofInvalid extends Schema.TaggedErrorClass<EnvironmentLinkProofInvalid>()(
37 "EnvironmentLinkProofInvalid",
38 {
39 environmentId: Schema.String,
40 reason: RelayEnvironmentLinkProofInvalidReason,
41 },
42) {
43 override get message(): string {
44 return `Environment '${this.environmentId}' link proof is invalid: ${this.reason}`;
45 }
46}
3447
3548export type EnvironmentLinkError =
3649 | EnvironmentLinkProofExpired
infra/relay/src/environments/EnvironmentLinks.ts+43-25
......@@ -5,10 +5,10 @@ import type {
55 RelayManagedEndpoint,
66} from "@t3tools/contracts/relay";
77import * as Context from "effect/Context";
8import * as Data from "effect/Data";
98import * as DateTime from "effect/DateTime";
109import * as Effect from "effect/Effect";
1110import * as Layer from "effect/Layer";
11import * as Schema from "effect/Schema";
1212import { and, eq, isNull, or } from "drizzle-orm";
1313
1414import { RelayDb } from "../db.ts";
......@@ -24,41 +24,59 @@ export interface AgentAwarenessDeliveryUserRecord {
2424 readonly liveActivitiesEnabled: boolean;
2525}
2626
27export class EnvironmentLinkUpsertPersistenceError extends Data.TaggedError(
27export class EnvironmentLinkUpsertPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkUpsertPersistenceError>()(
2828 "EnvironmentLinkUpsertPersistenceError",
29)<{
30 readonly cause: unknown;
31}> {}
29 { cause: Schema.Defect() },
30) {
31 override get message(): string {
32 return "Failed to persist environment link";
33 }
34}
3235
33export class EnvironmentLinkUserListPersistenceError extends Data.TaggedError(
36export class EnvironmentLinkUserListPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkUserListPersistenceError>()(
3437 "EnvironmentLinkUserListPersistenceError",
35)<{
36 readonly cause: unknown;
37}> {}
38 { cause: Schema.Defect() },
39) {
40 override get message(): string {
41 return "Failed to list users linked to environment";
42 }
43}
3844
39export class EnvironmentPublicKeyListPersistenceError extends Data.TaggedError(
45export class EnvironmentPublicKeyListPersistenceError extends Schema.TaggedErrorClass<EnvironmentPublicKeyListPersistenceError>()(
4046 "EnvironmentPublicKeyListPersistenceError",
41)<{
42 readonly cause: unknown;
43}> {}
47 { cause: Schema.Defect() },
48) {
49 override get message(): string {
50 return "Failed to list environment public keys";
51 }
52}
4453
45export class EnvironmentLinkListPersistenceError extends Data.TaggedError(
54export class EnvironmentLinkListPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkListPersistenceError>()(
4655 "EnvironmentLinkListPersistenceError",
47)<{
48 readonly cause: unknown;
49}> {}
56 { cause: Schema.Defect() },
57) {
58 override get message(): string {
59 return "Failed to list environment links";
60 }
61}
5062
51export class EnvironmentLinkLookupPersistenceError extends Data.TaggedError(
63export class EnvironmentLinkLookupPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkLookupPersistenceError>()(
5264 "EnvironmentLinkLookupPersistenceError",
53)<{
54 readonly cause: unknown;
55}> {}
65 { cause: Schema.Defect() },
66) {
67 override get message(): string {
68 return "Failed to look up environment link";
69 }
70}
5671
57export class EnvironmentLinkRevokePersistenceError extends Data.TaggedError(
72export class EnvironmentLinkRevokePersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkRevokePersistenceError>()(
5873 "EnvironmentLinkRevokePersistenceError",
59)<{
60 readonly cause: unknown;
61}> {}
74 { cause: Schema.Defect() },
75) {
76 override get message(): string {
77 return "Failed to revoke environment link";
78 }
79}
6280
6381export interface EnvironmentLinksShape {
6482 readonly upsert: (input: {
infra/relay/src/environments/EnvironmentPublishSignatures.ts+27-13
......@@ -11,7 +11,6 @@ import {
1111import { stableStringify } from "@t3tools/shared/relaySigning";
1212import * as Context from "effect/Context";
1313import * as Crypto from "effect/Crypto";
14import * as Data from "effect/Data";
1514import * as DateTime from "effect/DateTime";
1615import * as Effect from "effect/Effect";
1716import * as Encoding from "effect/Encoding";
......@@ -21,23 +20,38 @@ import * as Schema from "effect/Schema";
2120import * as DpopProofs from "../auth/DpopProofs.ts";
2221import * as RelayConfiguration from "../Config.ts";
2322
24export class EnvironmentPublishSignatureExpired extends Data.TaggedError(
23export class EnvironmentPublishSignatureExpired extends Schema.TaggedErrorClass<EnvironmentPublishSignatureExpired>()(
2524 "EnvironmentPublishSignatureExpired",
26)<{
27 readonly expiresAt: string;
28}> {}
25 {
26 expiresAt: Schema.String,
27 },
28) {
29 override get message(): string {
30 return `Environment publish signature expired at ${this.expiresAt}`;
31 }
32}
2933
30export class EnvironmentPublishSignatureInvalid extends Data.TaggedError(
34export class EnvironmentPublishSignatureInvalid extends Schema.TaggedErrorClass<EnvironmentPublishSignatureInvalid>()(
3135 "EnvironmentPublishSignatureInvalid",
32)<{
33 readonly environmentId: string;
34}> {}
36 {
37 environmentId: Schema.String,
38 },
39) {
40 override get message(): string {
41 return `Environment '${this.environmentId}' publish signature is invalid`;
42 }
43}
3544
36export class EnvironmentPublishPublicKeyMissing extends Data.TaggedError(
45export class EnvironmentPublishPublicKeyMissing extends Schema.TaggedErrorClass<EnvironmentPublishPublicKeyMissing>()(
3746 "EnvironmentPublishPublicKeyMissing",
38)<{
39 readonly environmentId: string;
40}> {}
47 {
48 environmentId: Schema.String,
49 },
50) {
51 override get message(): string {
52 return `Environment '${this.environmentId}' has no publish public key`;
53 }
54}
4155
4256export type EnvironmentPublishSignatureError =
4357 | EnvironmentPublishSignatureExpired
infra/relay/src/environments/ManagedEndpointAllocations.ts+12-6
......@@ -1,10 +1,10 @@
11import type { RelayManagedEndpoint } from "@t3tools/contracts/relay";
22import { and, eq } from "drizzle-orm";
33import * as Context from "effect/Context";
4import * as Data from "effect/Data";
54import * as DateTime from "effect/DateTime";
65import * as Effect from "effect/Effect";
76import * as Layer from "effect/Layer";
7import * as Schema from "effect/Schema";
88
99import { RelayDb } from "../db.ts";
1010import { isManagedEndpointHostname, managedEndpointForHostname } from "../deploymentConfig.ts";
......@@ -36,11 +36,17 @@ export function resolveReadyManagedEndpoint(input: {
3636 return managedEndpointForHostname(input.allocation.hostname);
3737}
3838
39export class ManagedEndpointAllocationPersistenceError extends Data.TaggedError(
39export class ManagedEndpointAllocationPersistenceError extends Schema.TaggedErrorClass<ManagedEndpointAllocationPersistenceError>()(
4040 "ManagedEndpointAllocationPersistenceError",
41)<{
42 readonly cause: unknown;
43}> {}
41 { cause: Schema.Defect() },
42) {
43 override get message(): string {
44 return "Failed to persist managed endpoint allocation";
45 }
46}
47const isManagedEndpointAllocationPersistenceError = Schema.is(
48 ManagedEndpointAllocationPersistenceError,
49);
4450
4551interface ManagedEndpointAllocationKey {
4652 readonly userId: string;
......@@ -98,7 +104,7 @@ const whereAllocation = (input: ManagedEndpointAllocationKey) =>
98104 );
99105
100106const persistenceError = (cause: unknown) =>
101 cause instanceof ManagedEndpointAllocationPersistenceError
107 isManagedEndpointAllocationPersistenceError(cause)
102108 ? cause
103109 : new ManagedEndpointAllocationPersistenceError({ cause });
104110
infra/relay/src/environments/ManagedEndpointProvider.ts+46-24
......@@ -3,12 +3,12 @@ import * as Cloudflare from "alchemy/Cloudflare";
33import * as Arr from "effect/Array";
44import * as Context from "effect/Context";
55import * as Crypto from "effect/Crypto";
6import * as Data from "effect/Data";
76import * as Effect from "effect/Effect";
87import * as Encoding from "effect/Encoding";
98import * as Layer from "effect/Layer";
109import * as Option from "effect/Option";
1110import * as Result from "effect/Result";
11import * as Schema from "effect/Schema";
1212
1313import type {
1414 RelayManagedEndpoint,
......@@ -25,28 +25,44 @@ import {
2525} from "../deploymentConfig.ts";
2626import { ManagedEndpointAllocations } from "./ManagedEndpointAllocations.ts";
2727
28export class ManagedEndpointProvisioningNotConfigured extends Data.TaggedError(
28export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass<ManagedEndpointProvisioningNotConfigured>()(
2929 "ManagedEndpointProvisioningNotConfigured",
30)<{}> {}
30 {},
31) {
32 override get message(): string {
33 return "Managed endpoint provisioning is not configured";
34 }
35}
3136
32export class ManagedEndpointProvisioningFailed extends Data.TaggedError(
37export class ManagedEndpointProvisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointProvisioningFailed>()(
3338 "ManagedEndpointProvisioningFailed",
34)<{
35 readonly cause: unknown;
36}> {}
39 { cause: Schema.Defect() },
40) {
41 override get message(): string {
42 return "Managed endpoint provisioning failed";
43 }
44}
3745
38export class ManagedEndpointDeprovisioningFailed extends Data.TaggedError(
46export class ManagedEndpointDeprovisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointDeprovisioningFailed>()(
3947 "ManagedEndpointDeprovisioningFailed",
40)<{
41 readonly cause: unknown;
42}> {}
48 { cause: Schema.Defect() },
49) {
50 override get message(): string {
51 return "Managed endpoint deprovisioning failed";
52 }
53}
4354
44export class ManagedEndpointOriginNotAllowed extends Data.TaggedError(
55export class ManagedEndpointOriginNotAllowed extends Schema.TaggedErrorClass<ManagedEndpointOriginNotAllowed>()(
4556 "ManagedEndpointOriginNotAllowed",
46)<{
47 readonly host: string;
48 readonly port: number;
49}> {}
57 {
58 host: Schema.String,
59 port: Schema.Number,
60 },
61) {
62 override get message(): string {
63 return `Managed endpoint origin '${this.host}:${this.port}' is not allowed`;
64 }
65}
5066
5167export type ManagedEndpointProviderError =
5268 | ManagedEndpointProvisioningNotConfigured
......@@ -80,11 +96,14 @@ interface ManagedEndpointTunnel {
8096 readonly name?: string | null;
8197}
8298
83export class ManagedEndpointTunnelClientError extends Data.TaggedError(
99export class ManagedEndpointTunnelClientError extends Schema.TaggedErrorClass<ManagedEndpointTunnelClientError>()(
84100 "ManagedEndpointTunnelClientError",
85)<{
86 readonly cause: unknown;
87}> {}
101 { cause: Schema.Defect() },
102) {
103 override get message(): string {
104 return "Managed endpoint tunnel provider request failed";
105 }
106}
88107
89108export interface ManagedEndpointTunnelClientShape {
90109 readonly list: (request: {
......@@ -124,11 +143,14 @@ interface ManagedEndpointCnameRecordInput {
124143 readonly proxied: true;
125144}
126145
127export class ManagedEndpointDnsClientError extends Data.TaggedError(
146export class ManagedEndpointDnsClientError extends Schema.TaggedErrorClass<ManagedEndpointDnsClientError>()(
128147 "ManagedEndpointDnsClientError",
129)<{
130 readonly cause: unknown;
131}> {}
148 { cause: Schema.Defect() },
149) {
150 override get message(): string {
151 return "Managed endpoint DNS provider request failed";
152 }
153}
132154
133155export interface ManagedEndpointDnsClientShape {
134156 readonly listRecords: (
infra/relay/src/http/Api.ts+11-8
......@@ -1,6 +1,5 @@
11import { createClerkClient, verifyToken } from "@clerk/backend";
22import { sql as drizzleSql } from "drizzle-orm";
3import * as Data from "effect/Data";
43import * as Crypto from "effect/Crypto";
54import * as Context from "effect/Context";
65import * as DateTime from "effect/DateTime";
......@@ -813,9 +812,16 @@ export const serverApi = HttpApiBuilder.group(
813812 }),
814813);
815814
816class ClerkTokenVerificationFailed extends Data.TaggedError("ClerkTokenVerificationFailed")<{
817 readonly cause: unknown;
818}> {}
815class ClerkTokenVerificationFailed extends Schema.TaggedErrorClass<ClerkTokenVerificationFailed>()(
816 "ClerkTokenVerificationFailed",
817 {
818 cause: Schema.Defect(),
819 },
820) {
821 override get message(): string {
822 return "Clerk token verification failed";
823 }
824}
819825
820826const isHttpUnauthorized = Schema.is(HttpApiError.Unauthorized);
821827
......@@ -846,16 +852,13 @@ const COMMON_AUTH_INVALID_REASONS = [
846852 DeliveryAttempts.DeliveryAttemptRecordPersistenceError,
847853] as const;
848854type RelayCommonPersistenceError = InstanceType<(typeof COMMON_AUTH_INVALID_REASONS)[number]>;
855const isRelayCommonPersistenceError = Schema.is(Schema.Union(COMMON_AUTH_INVALID_REASONS));
849856
850857type MapRelayCommonApiError<E> =
851858 | Exclude<E, HttpApiError.Unauthorized | RelayCommonPersistenceError>
852859 | (Extract<E, HttpApiError.Unauthorized> extends never ? never : RelayAuthInvalidError)
853860 | (Extract<E, RelayCommonPersistenceError> extends never ? never : RelayInternalError);
854861
855function isRelayCommonPersistenceError(error: unknown): error is RelayCommonPersistenceError {
856 return COMMON_AUTH_INVALID_REASONS.some((ErrorType) => error instanceof ErrorType);
857}
858
859862function relayInternalErrorResponse(reason: RelayInternalError["reason"]) {
860863 return currentTraceId.pipe(
861864 Effect.flatMap((traceId) =>
infra/relay/src/observability.test.ts created+86
......@@ -0,0 +1,86 @@
1import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
2import { expect, it } from "@effect/vitest";
3import * as Deferred from "effect/Deferred";
4import * as Effect from "effect/Effect";
5import * as Redacted from "effect/Redacted";
6import * as Schema from "effect/Schema";
7import * as HttpServer from "effect/unstable/http/HttpServer";
8import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
10import type { OtlpTracer } from "effect/unstable/observability";
11
12import { EnvironmentMintRequestFailed } from "./environments/EnvironmentConnector.ts";
13import { makeRelayTraceLayer } from "./observability.ts";
14
15interface ExportedRequest {
16 readonly authorization: string | undefined;
17 readonly body: string;
18 readonly dataset: string | undefined;
19}
20
21const otlpAttributeValue = (value: {
22 readonly stringValue?: string | null;
23 readonly boolValue?: boolean | null;
24 readonly intValue?: number | null;
25 readonly doubleValue?: number | null;
26}) => value.stringValue ?? value.boolValue ?? value.intValue ?? value.doubleValue;
27
28const decodeJson = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString);
29
30it.effect("exports schema error fields as span attributes", () =>
31 Effect.gen(function* () {
32 const exportedRequest = yield* Deferred.make<ExportedRequest>();
33 yield* HttpServer.serveEffect(
34 Effect.gen(function* () {
35 const request = yield* HttpServerRequest.HttpServerRequest;
36 yield* Deferred.succeed(exportedRequest, {
37 authorization: request.headers.authorization,
38 body: yield* request.text,
39 dataset: request.headers["x-axiom-dataset"],
40 });
41 return HttpServerResponse.empty({ status: 204 });
42 }),
43 );
44
45 yield* Effect.fail(
46 new EnvironmentMintRequestFailed({
47 environmentId: "environment-1",
48 operation: "connect",
49 cause: new Error("upstream unavailable"),
50 }),
51 ).pipe(
52 Effect.withSpan("relay.test.schema_error"),
53 Effect.exit,
54 Effect.provide(
55 makeRelayTraceLayer({
56 tracesEndpoint: "/v1/traces",
57 tracesDatasetName: "relay-test-traces",
58 ingestToken: Redacted.make("test-token"),
59 }),
60 ),
61 );
62
63 const request = yield* Deferred.await(exportedRequest).pipe(Effect.timeout("1 second"));
64 const payload = (yield* decodeJson(request.body)) as OtlpTracer.TraceData;
65 const span = payload.resourceSpans
66 .flatMap((resourceSpan) => resourceSpan.scopeSpans)
67 .flatMap((scopeSpan) => scopeSpan.spans)
68 .find((candidate) => candidate.name === "relay.test.schema_error");
69 const attributes = Object.fromEntries(
70 (span?.attributes ?? []).map((attribute) => [
71 attribute.key,
72 otlpAttributeValue(attribute.value),
73 ]),
74 );
75
76 expect(request.authorization).toBe("Bearer test-token");
77 expect(request.dataset).toBe("relay-test-traces");
78 expect(attributes).toMatchObject({
79 "error.type": "EnvironmentMintRequestFailed",
80 "error.environmentId": "environment-1",
81 "error.operation": "connect",
82 "error.cause.name": "Error",
83 "error.cause.message": "upstream unavailable",
84 });
85 }).pipe(Effect.provide(NodeHttpServer.layerTest), Effect.scoped),
86);
infra/relay/src/observability.ts+160-15
......@@ -1,9 +1,14 @@
11import * as Alchemy from "alchemy";
22import * as Axiom from "alchemy/Axiom";
33import * as Output from "alchemy/Output";
4import * as Layer from "effect/Layer";
4import * as Cause from "effect/Cause";
55import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";
69import * as Redacted from "effect/Redacted";
10import * as Schema from "effect/Schema";
11import * as Tracer from "effect/Tracer";
712import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
813
914import { relayResourceNameForStage } from "./deploymentConfig.ts";
......@@ -54,23 +59,163 @@ export const withSpanAttributes =
5459 Effect.andThen(effect.pipe(Effect.annotateSpans(attributes))),
5560 );
5661
62const appendEncodedAttributes = (
63 attributes: Record<string, unknown>,
64 prefix: string,
65 value: unknown,
66): void => {
67 if (
68 value === null ||
69 typeof value === "string" ||
70 typeof value === "number" ||
71 typeof value === "boolean" ||
72 typeof value === "bigint" ||
73 Array.isArray(value)
74 ) {
75 attributes[prefix] = value;
76 return;
77 }
78 if (typeof value !== "object") {
79 return;
80 }
81 for (const [key, child] of Object.entries(value)) {
82 appendEncodedAttributes(attributes, `${prefix}.${key}`, child);
83 }
84};
85
86const schemaErrorAttributes = (error: unknown): Record<string, unknown> | undefined => {
87 if (typeof error !== "object" || error === null) {
88 return undefined;
89 }
90 const constructor = error.constructor;
91 if (!Schema.isSchema(constructor)) {
92 return undefined;
93 }
94 const encoded = Schema.encodeUnknownOption(constructor as unknown as Schema.Encoder<unknown>)(
95 error,
96 );
97 if (Option.isNone(encoded) || typeof encoded.value !== "object" || encoded.value === null) {
98 return undefined;
99 }
100 const tag = Reflect.get(encoded.value, "_tag");
101 if (typeof tag !== "string") {
102 return undefined;
103 }
104
105 const attributes: Record<string, unknown> = {
106 "error.type": tag,
107 };
108 for (const [key, value] of Object.entries(encoded.value)) {
109 if (key !== "_tag") {
110 appendEncodedAttributes(attributes, `error.${key}`, value);
111 }
112 }
113 return attributes;
114};
115
116const annotateSchemaError = (span: Tracer.Span, exit: Exit.Exit<unknown, unknown>): void => {
117 if (Exit.isSuccess(exit)) {
118 return;
119 }
120 for (const reason of exit.cause.reasons) {
121 const error = Cause.isFailReason(reason)
122 ? reason.error
123 : Cause.isDieReason(reason)
124 ? reason.defect
125 : undefined;
126 const attributes = schemaErrorAttributes(error);
127 if (attributes) {
128 for (const [key, value] of Object.entries(attributes)) {
129 span.attribute(key, value);
130 }
131 return;
132 }
133 }
134};
135
136class RelayTraceSpan implements Tracer.Span {
137 readonly _tag = "Span";
138 private readonly delegate: Tracer.Span;
139
140 constructor(delegate: Tracer.Span) {
141 this.delegate = delegate;
142 }
143
144 get name() {
145 return this.delegate.name;
146 }
147 get spanId() {
148 return this.delegate.spanId;
149 }
150 get traceId() {
151 return this.delegate.traceId;
152 }
153 get parent() {
154 return this.delegate.parent;
155 }
156 get annotations() {
157 return this.delegate.annotations;
158 }
159 get status() {
160 return this.delegate.status;
161 }
162 get attributes() {
163 return this.delegate.attributes;
164 }
165 get links() {
166 return this.delegate.links;
167 }
168 get sampled() {
169 return this.delegate.sampled;
170 }
171 get kind() {
172 return this.delegate.kind;
173 }
174
175 end(endTime: bigint, exit: Exit.Exit<unknown, unknown>): void {
176 annotateSchemaError(this.delegate, exit);
177 this.delegate.end(endTime, exit);
178 }
179
180 attribute(key: string, value: unknown): void {
181 this.delegate.attribute(key, value);
182 }
183
184 event(name: string, startTime: bigint, attributes?: Record<string, unknown>): void {
185 this.delegate.event(name, startTime, attributes);
186 }
187
188 addLinks(links: ReadonlyArray<Tracer.SpanLink>): void {
189 this.delegate.addLinks(links);
190 }
191}
192
193const withSchemaErrorAttributes = (delegate: Tracer.Tracer): Tracer.Tracer =>
194 Tracer.make({
195 span: (options) => new RelayTraceSpan(delegate.span(options)),
196 ...(delegate.context ? { context: delegate.context } : {}),
197 });
198
57199export const makeRelayTraceLayer = (input: {
58200 readonly tracesEndpoint: string;
59201 readonly tracesDatasetName: string;
60202 readonly ingestToken: Redacted.Redacted<string>;
61203}) =>
62 OtlpTracer.layer({
63 url: input.tracesEndpoint,
64 resource: {
65 serviceName: "t3-code-relay-worker",
66 attributes: {
67 "service.runtime": "cloudflare-worker",
68 "service.component": "relay",
204 Layer.effect(
205 Tracer.Tracer,
206 OtlpTracer.make({
207 url: input.tracesEndpoint,
208 resource: {
209 serviceName: "t3-code-relay-worker",
210 attributes: {
211 "service.runtime": "cloudflare-worker",
212 "service.component": "relay",
213 },
214 },
215 headers: {
216 Authorization: `Bearer ${Redacted.value(input.ingestToken)}`,
217 "X-Axiom-Dataset": input.tracesDatasetName,
69218 },
70 },
71 headers: {
72 Authorization: `Bearer ${Redacted.value(input.ingestToken)}`,
73 "X-Axiom-Dataset": input.tracesDatasetName,
74 },
75 exportInterval: "1 second",
76 }).pipe(Layer.provide(OtlpSerialization.layerJson));
219 exportInterval: "1 second",
220 }).pipe(Effect.map(withSchemaErrorAttributes)),
221 ).pipe(Layer.provide(OtlpSerialization.layerJson));
packages/contracts/src/relay.ts+55-11
......@@ -329,7 +329,11 @@ export class RelayAuthInvalidError extends Schema.TaggedErrorClass<RelayAuthInva
329329 traceId: TrimmedNonEmptyString,
330330 },
331331 { httpApiStatus: 401 },
332) {}
332) {
333 override get message(): string {
334 return `Relay authentication failed: ${this.reason}`;
335 }
336}
333337
334338export class RelayEnvironmentLinkProofExpiredError extends Schema.TaggedErrorClass<RelayEnvironmentLinkProofExpiredError>()(
335339 "RelayEnvironmentLinkProofExpiredError",
......@@ -338,7 +342,11 @@ export class RelayEnvironmentLinkProofExpiredError extends Schema.TaggedErrorCla
338342 traceId: TrimmedNonEmptyString,
339343 },
340344 { httpApiStatus: 401 },
341) {}
345) {
346 override get message(): string {
347 return "Relay environment link proof expired";
348 }
349}
342350
343351export class RelayEnvironmentLinkProofInvalidError extends Schema.TaggedErrorClass<RelayEnvironmentLinkProofInvalidError>()(
344352 "RelayEnvironmentLinkProofInvalidError",
......@@ -348,7 +356,11 @@ export class RelayEnvironmentLinkProofInvalidError extends Schema.TaggedErrorCla
348356 traceId: TrimmedNonEmptyString,
349357 },
350358 { httpApiStatus: 400 },
351) {}
359) {
360 override get message(): string {
361 return `Relay environment link proof is invalid: ${this.reason}`;
362 }
363}
352364
353365export class RelayEnvironmentConnectNotAuthorizedError extends Schema.TaggedErrorClass<RelayEnvironmentConnectNotAuthorizedError>()(
354366 "RelayEnvironmentConnectNotAuthorizedError",
......@@ -357,7 +369,11 @@ export class RelayEnvironmentConnectNotAuthorizedError extends Schema.TaggedErro
357369 traceId: TrimmedNonEmptyString,
358370 },
359371 { httpApiStatus: 403 },
360) {}
372) {
373 override get message(): string {
374 return "Relay environment connection is not authorized";
375 }
376}
361377
362378export class RelayEnvironmentEndpointUnavailableError extends Schema.TaggedErrorClass<RelayEnvironmentEndpointUnavailableError>()(
363379 "RelayEnvironmentEndpointUnavailableError",
......@@ -367,7 +383,11 @@ export class RelayEnvironmentEndpointUnavailableError extends Schema.TaggedError
367383 traceId: TrimmedNonEmptyString,
368384 },
369385 { httpApiStatus: 502 },
370) {}
386) {
387 override get message(): string {
388 return `Relay environment endpoint is unavailable: ${this.reason}`;
389 }
390}
371391
372392export class RelayEnvironmentEndpointTimedOutError extends Schema.TaggedErrorClass<RelayEnvironmentEndpointTimedOutError>()(
373393 "RelayEnvironmentEndpointTimedOutError",
......@@ -376,7 +396,11 @@ export class RelayEnvironmentEndpointTimedOutError extends Schema.TaggedErrorCla
376396 traceId: TrimmedNonEmptyString,
377397 },
378398 { httpApiStatus: 504 },
379) {}
399) {
400 override get message(): string {
401 return "Relay environment endpoint request timed out";
402 }
403}
380404
381405export class RelayEnvironmentLinkFailedError extends Schema.TaggedErrorClass<RelayEnvironmentLinkFailedError>()(
382406 "RelayEnvironmentLinkFailedError",
......@@ -386,7 +410,11 @@ export class RelayEnvironmentLinkFailedError extends Schema.TaggedErrorClass<Rel
386410 traceId: TrimmedNonEmptyString,
387411 },
388412 { httpApiStatus: 500 },
389) {}
413) {
414 override get message(): string {
415 return `Relay environment link failed: ${this.reason}`;
416 }
417}
390418
391419export class RelayEnvironmentLinkUnavailableError extends Schema.TaggedErrorClass<RelayEnvironmentLinkUnavailableError>()(
392420 "RelayEnvironmentLinkUnavailableError",
......@@ -396,7 +424,11 @@ export class RelayEnvironmentLinkUnavailableError extends Schema.TaggedErrorClas
396424 traceId: TrimmedNonEmptyString,
397425 },
398426 { httpApiStatus: 503 },
399) {}
427) {
428 override get message(): string {
429 return `Relay environment link is unavailable: ${this.reason}`;
430 }
431}
400432
401433export class RelayAgentActivityPublishProofExpiredError extends Schema.TaggedErrorClass<RelayAgentActivityPublishProofExpiredError>()(
402434 "RelayAgentActivityPublishProofExpiredError",
......@@ -405,7 +437,11 @@ export class RelayAgentActivityPublishProofExpiredError extends Schema.TaggedErr
405437 traceId: TrimmedNonEmptyString,
406438 },
407439 { httpApiStatus: 401 },
408) {}
440) {
441 override get message(): string {
442 return "Relay agent activity publish proof expired";
443 }
444}
409445
410446export class RelayAgentActivityPublishProofInvalidError extends Schema.TaggedErrorClass<RelayAgentActivityPublishProofInvalidError>()(
411447 "RelayAgentActivityPublishProofInvalidError",
......@@ -415,7 +451,11 @@ export class RelayAgentActivityPublishProofInvalidError extends Schema.TaggedErr
415451 traceId: TrimmedNonEmptyString,
416452 },
417453 { httpApiStatus: 401 },
418) {}
454) {
455 override get message(): string {
456 return `Relay agent activity publish proof is invalid: ${this.reason}`;
457 }
458}
419459
420460export class RelayInternalError extends Schema.TaggedErrorClass<RelayInternalError>()(
421461 "RelayInternalError",
......@@ -425,7 +465,11 @@ export class RelayInternalError extends Schema.TaggedErrorClass<RelayInternalErr
425465 traceId: TrimmedNonEmptyString,
426466 },
427467 { httpApiStatus: 500 },
428) {}
468) {
469 override get message(): string {
470 return `Relay internal error: ${this.reason}`;
471 }
472}
429473
430474export const RelayProtectedError = Schema.Union([
431475 RelayAuthInvalidError,