History

[codex] Preserve APNs delivery failure context (#3475)

Julius Marminge committed 4 months ago

Commit e3970e71 parent 3c246f52 files · +124 −17Browse files

Unified · Split

2 files
  1. infra/relay/src/agentActivity/ApnsDeliveries.test.ts +36 −1
  2. infra/relay/src/agentActivity/ApnsDeliveries.ts +88 −16
infra/relay/src/agentActivity/ApnsDeliveries.test.ts+36−1
@@ -7,7 +7,9 @@ import { describe, expect, it } from "@effect/vitest"; · 6 unmodified lines
77 import * as NodeCrypto from "node:crypto";
88 import * as Effect from "effect/Effect";
99 import * as Layer from "effect/Layer";
10+import * as Logger from "effect/Logger";
1011 import * as Redacted from "effect/Redacted";
12+import * as References from "effect/References";
1113 import {
1214 FetchHttpClient,
1315 HttpClient,
@@ -629,6 +631,17 @@ describe("ApnsDeliveries", () => { · 615 unmodified lines
629631
630632 it.effect("processes signed jobs through APNs and records attempts", () => {
631633 const attempts: Array<DeliveryAttempts.DeliveryAttemptInput> = [];
634+ const transportErrors: Array<ApnsDeliveries.ApnsDeliveryTransportError> = [];
635+ const logger = Logger.make(({ fiber }) => {
636+ const annotation = fiber.getRef(References.CurrentLogAnnotations).error;
637+ if (!Redacted.isRedacted(annotation)) {
638+ return;
639+ }
640+ const error = Redacted.value(annotation);
641+ if (ApnsDeliveries.isApnsDeliveryTransportError(error)) {
642+ transportErrors.push(error);
643+ }
644+ });
632645 const payload = makeApnsDeliveryJobPayload({
633646 kind: "live_activity_update",
634647 userId: target.user_id,
@@ -657,7 +670,29 @@ describe("ApnsDeliveries", () => { · 22 unmodified lines
657670 token: "activity-token",
658671 },
659672 ]);
660− }).pipe(Effect.provide(makeLayer({ attempts })));
673+ expect(transportErrors).toHaveLength(1);
674+ const error = transportErrors[0]!;
675+ expect(error).toMatchObject({
676+ deviceId: target.device_id,
677+ kind: "live_activity_update",
678+ sourceJobId: "job-1",
679+ apnsErrorTag: "ApnsJwtSigningError",
680+ requestStage: null,
681+ });
682+ expect(error.cause).toBeInstanceOf(ApnsClient.ApnsJwtSigningError);
683+ expect(error.cause).toMatchObject({
684+ teamId: "team-id",
685+ keyId: "key-id",
686+ });
687+ expect((error.cause as ApnsClient.ApnsJwtSigningError).cause).toBeDefined();
688+ }).pipe(
689+ Effect.provide(
690+ Layer.mergeAll(
691+ makeLayer({ attempts }),
692+ Logger.layer([logger], { mergeWithExisting: false }),
693+ ),
694+ ),
695+ );
661696 });
662697
663698 it.effect("processes signed push notification jobs through APNs and records attempts", () => {
infra/relay/src/agentActivity/ApnsDeliveries.ts+88−16
@@ -7,12 +7,14 @@ import type { · 6 unmodified lines
77 import {
88 RelayAgentActivityAggregateState as RelayAgentActivityAggregateStateSchema,
99 RelayAgentAwarenessPreferences as RelayAgentAwarenessPreferencesSchema,
10+ RelayDeliveryKind as RelayDeliveryKindSchema,
1011 } from "@t3tools/contracts/relay";
1112 import * as Context from "effect/Context";
1213 import * as DateTime from "effect/DateTime";
1314 import * as Effect from "effect/Effect";
1415 import * as Layer from "effect/Layer";
1516 import * as Option from "effect/Option";
17+import * as Redacted from "effect/Redacted";
1618 import * as Schema from "effect/Schema";
1719
1820 import {
@@ -87,6 +89,28 @@ export class ApnsDeliveryJobClaimInFlight extends Schema.TaggedErrorClass<ApnsDe · 68 unmodified lines
8789 }
8890 }
8991
92+export class ApnsDeliveryTransportError extends Schema.TaggedErrorClass<ApnsDeliveryTransportError>()(
93+ "ApnsDeliveryTransportError",
94+ {
95+ deviceId: Schema.String,
96+ kind: RelayDeliveryKindSchema,
97+ sourceJobId: Schema.NullOr(Schema.String),
98+ apnsErrorTag: Schema.Literals([
99+ "ApnsJwtEncodingError",
100+ "ApnsJwtSigningError",
101+ "ApnsHttpRequestError",
102+ ]),
103+ requestStage: Schema.NullOr(Schema.Literals(["send", "read-response"])),
104+ cause: Schema.Defect(),
105+ },
106+) {
107+ override get message(): string {
108+ return `APNs ${this.kind} delivery failed for device ${this.deviceId}.`;
109+ }
110+}
111+
112+export const isApnsDeliveryTransportError = Schema.is(ApnsDeliveryTransportError);
113+
90114 const decodeRelayAgentActivityAggregateStateJson = Schema.decodeUnknownOption(
91115 Schema.fromJsonString(RelayAgentActivityAggregateStateSchema),
92116 );
@@ -302,6 +326,42 @@ function deliveryAttemptOutcome(result: Apns.ApnsDeliveryResult) { · 209 unmodified lines
302326 };
303327 }
304328
329+const recoverApnsDeliveryTransportError = (
330+ input: {
331+ readonly deviceId: string;
332+ readonly kind: RelayDeliveryKind;
333+ readonly sourceJobId: string | null;
334+ },
335+ cause: Apns.ApnsError,
336+): Effect.Effect<Apns.ApnsDeliveryResult> => {
337+ const error = new ApnsDeliveryTransportError({
338+ deviceId: input.deviceId,
339+ kind: input.kind,
340+ sourceJobId: input.sourceJobId,
341+ apnsErrorTag: cause._tag,
342+ requestStage: cause._tag === "ApnsHttpRequestError" ? cause.stage : null,
343+ cause,
344+ });
345+ return Effect.logError(error.message).pipe(
346+ Effect.annotateLogs({
347+ error: Redacted.make(error, { label: error._tag }),
348+ "error.type": error._tag,
349+ "error.apns_error_tag": error.apnsErrorTag,
350+ ...(error.requestStage === null ? {} : { "error.request_stage": error.requestStage }),
351+ ...(error.stack === undefined ? {} : { "error.stack": error.stack }),
352+ "relay.mobile.device_id": error.deviceId,
353+ "relay.delivery.kind": error.kind,
354+ ...(error.sourceJobId === null ? {} : { "relay.delivery.job_id": error.sourceJobId }),
355+ }),
356+ Effect.as({
357+ ok: false,
358+ status: 0,
359+ reason: cause.message,
360+ apnsId: null,
361+ }),
362+ );
363+};
364+
305365 interface LiveActivityDeliveryTarget {
306366 readonly user_id: string;
307367 readonly device_id: string;
@@ -440,6 +500,15 @@ export const make = Effect.gen(function* () { · 132 unmodified lines
440500 { ...input, aggregate } as SendLiveActivityDeliveryInput,
441501 now,
442502 );
503+ const recoverTransportError = (cause: Apns.ApnsError) =>
504+ recoverApnsDeliveryTransportError(
505+ {
506+ deviceId: input.target.device_id,
507+ kind: input.kind,
508+ sourceJobId: input.sourceJobId ?? null,
509+ },
510+ cause,
511+ );
443512 if (input.sourceJobId) {
444513 const claim = yield* attempts.claimSourceJob({
445514 userId: input.target.user_id,
@@ -476,14 +545,11 @@ export const make = Effect.gen(function* () { · 30 unmodified lines
476545 issuedAtUnixSeconds: epochSeconds,
477546 })
478547 .pipe(
479− Effect.catch((error) =>
480− Effect.succeed({
481− ok: false,
482− status: 0,
483− reason: error.message,
484− apnsId: null,
485− }),
486− ),
548+ Effect.catchTags({
549+ ApnsJwtEncodingError: recoverTransportError,
550+ ApnsJwtSigningError: recoverTransportError,
551+ ApnsHttpRequestError: recoverTransportError,
552+ }),
487553 );
488554 if (result.ok) {
489555 yield* liveActivities.markDelivery({
@@ -551,6 +617,15 @@ export const make = Effect.gen(function* () { · 61 unmodified lines
551617 token: input.token,
552618 notification,
553619 });
620+ const recoverTransportError = (cause: Apns.ApnsError) =>
621+ recoverApnsDeliveryTransportError(
622+ {
623+ deviceId: input.target.device_id,
624+ kind: "push_notification",
625+ sourceJobId: input.sourceJobId ?? null,
626+ },
627+ cause,
628+ );
554629 if (input.sourceJobId) {
555630 const claim = yield* attempts.claimSourceJob({
556631 userId: input.target.user_id,
@@ -593,14 +668,11 @@ export const make = Effect.gen(function* () { · 36 unmodified lines
593668 issuedAtUnixSeconds: epochSeconds,
594669 })
595670 .pipe(
596− Effect.catch((error) =>
597− Effect.succeed({
598− ok: false,
599− status: 0,
600− reason: error.message,
601− apnsId: null,
602− }),
603− ),
671+ Effect.catchTags({
672+ ApnsJwtEncodingError: recoverTransportError,
673+ ApnsJwtSigningError: recoverTransportError,
674+ ApnsHttpRequestError: recoverTransportError,
675+ }),
604676 );
605677 if (isPermanentApnsTokenFailure(result)) {
606678 yield* liveActivities.invalidateDeliveryToken({

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t