Skip to content

Commit fdbe817

Browse files
authored
Add breadcrumbs to CDN Worker (#5552)
1 parent 40ec21d commit fdbe817

File tree

9 files changed

+173
-29
lines changed

9 files changed

+173
-29
lines changed

packages/services/cdn-worker/src/artifact-handler.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as itty from 'itty-router';
22
import zod from 'zod';
33
import { createAnalytics, type Analytics } from './analytics';
44
import { type ArtifactStorageReader, type ArtifactsType } from './artifact-storage-reader';
5+
import { createBreadcrumb, type Breadcrumb } from './breadcrumbs';
56
import { InvalidAuthKeyResponse, MissingAuthKeyResponse } from './errors';
67
import { IsAppDeploymentActive } from './is-app-deployment-active';
78
import type { KeyValidator } from './key-validation';
@@ -21,6 +22,7 @@ type ArtifactRequestHandler = {
2122
isKeyValid: KeyValidator;
2223
isAppDeploymentActive: IsAppDeploymentActive;
2324
analytics?: Analytics;
25+
breadcrumb?: Breadcrumb;
2426
fallback?: (
2527
request: Request,
2628
params: { targetId: string; artifactType: string },
@@ -60,6 +62,7 @@ const authHeaderName = 'x-hive-cdn-key' as const;
6062
export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
6163
const router = itty.Router<itty.IRequest & Request>();
6264
const analytics = deps.analytics ?? createAnalytics();
65+
const breadcrumb = deps.breadcrumb ?? createBreadcrumb();
6366

6467
const authenticate = async (
6568
request: itty.IRequest & Request,
@@ -100,8 +103,13 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
100103

101104
const params = parseResult.data;
102105

106+
breadcrumb(
107+
`Artifact v1 handler (type=${params.artifactType}, targetId=${params.targetId}, contractName=${params.contractName})`,
108+
);
109+
103110
/** Legacy handling for old client SDK versions. */
104111
if (params.artifactType === 'schema') {
112+
breadcrumb('Redirecting from /schema to /services');
105113
return createResponse(
106114
analytics,
107115
'Found.',

packages/services/cdn-worker/src/artifact-storage-reader.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import zod from 'zod';
22
import type { Analytics } from './analytics';
33
import { AwsClient } from './aws';
4+
import type { Breadcrumb } from './breadcrumbs';
45

56
export function buildArtifactStorageKey(
67
targetId: string,
@@ -56,6 +57,8 @@ export function buildAppDeploymentIsEnabledKey(
5657
* Read an artifact/app deployment operation from S3.
5758
*/
5859
export class ArtifactStorageReader {
60+
private breadcrumb: Breadcrumb;
61+
5962
constructor(
6063
private s3: {
6164
client: AwsClient;
@@ -68,9 +71,12 @@ export class ArtifactStorageReader {
6871
bucketName: string;
6972
} | null,
7073
private analytics: Analytics | null,
74+
breadcrumb: Breadcrumb | null,
7175
/** Timeout in milliseconds for S3 read calls. */
7276
private timeout: number = 5_000,
73-
) {}
77+
) {
78+
this.breadcrumb = breadcrumb ?? (() => {});
79+
}
7480

7581
/**
7682
* Perform a request to S3, with retries and optional mirror.
@@ -122,9 +128,11 @@ export class ArtifactStorageReader {
122128
},
123129
})
124130
.catch(err => {
131+
this.breadcrumb('Failed to fetch from primary');
125132
if (!this.s3Mirror) {
126133
return Promise.reject(err);
127134
}
135+
this.breadcrumb('Fetching from primary and mirror now');
128136
// Use two AbortSignals to avoid a situation
129137
// where Response.body is consumed,
130138
// but the request was aborted after being resolved.
@@ -201,11 +209,18 @@ export class ArtifactStorageReader {
201209
artifactType = 'sdl';
202210
}
203211

212+
this.breadcrumb(
213+
`Reading artifact (targetId=${targetId}, artifactType=${artifactType}, contractName=${contractName})`,
214+
);
215+
204216
const key = buildArtifactStorageKey(targetId, artifactType, contractName);
205217

218+
this.breadcrumb(`Reading artifact from S3 key: ${key}`);
219+
206220
const headers: HeadersInit = {};
207221

208222
if (etagValue) {
223+
this.breadcrumb('if-none-match detected');
209224
headers['if-none-match'] = etagValue;
210225
}
211226

@@ -246,6 +261,8 @@ export class ArtifactStorageReader {
246261
} as const;
247262
}
248263

264+
this.breadcrumb(`Failed to read artifact`);
265+
249266
const body = await response.text();
250267
throw new Error(`GET request failed with status ${response.status}: ${body}`);
251268
}
@@ -330,8 +347,10 @@ export class ArtifactStorageReader {
330347
}
331348

332349
async readLegacyAccessKey(targetId: string) {
350+
const key = ['cdn-legacy-keys', targetId].join('/');
351+
this.breadcrumb(`Reading from S3 key: ${key}`);
333352
const response = await this.request({
334-
key: ['cdn-legacy-keys', targetId].join('/'),
353+
key,
335354
method: 'GET',
336355
onAttempt: args => {
337356
this.analytics?.track(
@@ -353,10 +372,11 @@ export class ArtifactStorageReader {
353372
}
354373

355374
async readAccessKey(targetId: string, keyId: string) {
356-
const s3KeyParts = ['cdn-keys', targetId, keyId];
375+
const key = ['cdn-keys', targetId, keyId].join('/');
376+
this.breadcrumb(`Reading from S3 key: ${key}`);
357377

358378
const response = await this.request({
359-
key: s3KeyParts.join('/'),
379+
key,
360380
method: 'GET',
361381
onAttempt: args => {
362382
this.analytics?.track(
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type Breadcrumb = (message: string) => void;
2+
3+
export function createBreadcrumb() {
4+
return (message: string) => {
5+
console.debug(message);
6+
};
7+
}

packages/services/cdn-worker/src/dev.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,18 @@ const s3 = {
2323
// eslint-disable-next-line no-process-env
2424
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 4010;
2525

26-
const artifactStorageReader = new ArtifactStorageReader(s3, null, null);
26+
const artifactStorageReader = new ArtifactStorageReader(s3, null, null, null);
2727

2828
const handleRequest = createRequestHandler({
2929
isKeyValid: createIsKeyValid({
3030
artifactStorageReader,
3131
getCache: null,
3232
waitUntil: null,
3333
analytics: null,
34+
breadcrumb: null,
35+
captureException(error) {
36+
console.error(error);
37+
},
3438
}),
3539
async getArtifactAction(targetId, contractName, artifactType, eTag) {
3640
return artifactStorageReader.readArtifact(targetId, contractName, artifactType, eTag);
@@ -52,6 +56,10 @@ const handleArtifactRequest = createArtifactRequestHandler({
5256
getCache: null,
5357
waitUntil: null,
5458
analytics: null,
59+
breadcrumb: null,
60+
captureException(error) {
61+
console.error(error);
62+
},
5563
}),
5664
isAppDeploymentActive: createIsAppDeploymentActive({
5765
artifactStorageReader,

packages/services/cdn-worker/src/handler.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { buildSchema, introspectionFromSchema } from 'graphql';
22
import { Analytics, createAnalytics } from './analytics';
33
import { GetArtifactActionFn } from './artifact-handler';
44
import { ArtifactsType as ModernArtifactsType } from './artifact-storage-reader';
5+
import { Breadcrumb, createBreadcrumb } from './breadcrumbs';
56
import {
67
CDNArtifactNotFound,
78
InvalidArtifactTypeResponse,
@@ -192,6 +193,7 @@ async function parseIncomingRequest(
192193
request: Request,
193194
keyValidator: KeyValidator,
194195
analytics: Analytics,
196+
breadcrumb: Breadcrumb,
195197
): Promise<
196198
| { error: Response }
197199
| {
@@ -239,6 +241,7 @@ async function parseIncomingRequest(
239241
legacyToModernArtifactTypeMap[artifactType],
240242
};
241243
} catch (e) {
244+
breadcrumb(`Failed to validate key for ${targetId}, error: ${e}`);
242245
console.warn(`Failed to validate key for ${targetId}, error:`, e);
243246
return {
244247
error: new InvalidAuthKeyResponse(analytics, request),
@@ -255,15 +258,22 @@ interface RequestHandlerDependencies {
255258
isKeyValid: IsKeyValid;
256259
getArtifactAction: GetArtifactActionFn;
257260
analytics?: Analytics;
261+
breadcrumb?: Breadcrumb;
258262
fetchText: (url: string) => Promise<string>;
259263
}
260264

261265
export function createRequestHandler(deps: RequestHandlerDependencies) {
262266
const analytics = deps.analytics ?? createAnalytics();
267+
const breadcrumb = deps.breadcrumb ?? createBreadcrumb();
263268
const artifactTypesHandlers = createArtifactTypesHandlers(analytics);
264269

265270
return async (request: Request): Promise<Response> => {
266-
const parsedRequest = await parseIncomingRequest(request, deps.isKeyValid, analytics);
271+
const parsedRequest = await parseIncomingRequest(
272+
request,
273+
deps.isKeyValid,
274+
analytics,
275+
breadcrumb,
276+
);
267277

268278
if ('error' in parsedRequest) {
269279
return parsedRequest.error;

packages/services/cdn-worker/src/index.ts

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,58 @@ const handler: ExportedHandler<Env> = {
7676
s3: env.S3_ANALYTICS,
7777
});
7878

79-
const artifactStorageReader = new ArtifactStorageReader(s3, s3Mirror, analytics);
79+
const sentry = new Toucan({
80+
dsn: env.SENTRY_DSN,
81+
environment: env.SENTRY_ENVIRONMENT,
82+
release: env.SENTRY_RELEASE,
83+
dist: 'cdn-worker',
84+
context: ctx,
85+
request,
86+
requestDataOptions: {
87+
allowedHeaders: [
88+
'user-agent',
89+
'cf-ipcountry',
90+
'accept-encoding',
91+
'accept',
92+
'x-real-ip',
93+
'cf-connecting-ip',
94+
],
95+
allowedSearchParams: /(.*)/,
96+
},
97+
});
98+
99+
const artifactStorageReader = new ArtifactStorageReader(
100+
s3,
101+
s3Mirror,
102+
analytics,
103+
(message: string) => sentry.addBreadcrumb({ message }),
104+
);
80105

81106
const isKeyValid = createIsKeyValid({
82107
waitUntil: p => ctx.waitUntil(p),
83108
getCache: () => caches.open('artifacts-auth'),
84109
artifactStorageReader,
85110
analytics,
111+
breadcrumb(message: string) {
112+
sentry.addBreadcrumb({
113+
message,
114+
});
115+
},
116+
captureException(error) {
117+
sentry.captureException(error);
118+
},
86119
});
87120

88121
const handleRequest = createRequestHandler({
89122
async getArtifactAction(targetId, contractName, artifactType, eTag) {
90123
return artifactStorageReader.readArtifact(targetId, contractName, artifactType, eTag);
91124
},
92125
isKeyValid,
126+
breadcrumb(message: string) {
127+
sentry.addBreadcrumb({
128+
message,
129+
});
130+
},
93131
analytics,
94132
async fetchText(url) {
95133
// Yeah, it's not globally defined, but it makes no sense to define it globally
@@ -134,6 +172,9 @@ const handler: ExportedHandler<Env> = {
134172
const handleArtifactRequest = createArtifactRequestHandler({
135173
isKeyValid,
136174
analytics,
175+
breadcrumb(message: string) {
176+
sentry.addBreadcrumb({ message });
177+
},
137178
artifactStorageReader,
138179
isAppDeploymentActive: createIsAppDeploymentActive({
139180
artifactStorageReader,
@@ -164,31 +205,16 @@ const handler: ExportedHandler<Env> = {
164205
// Legacy CDN Handlers
165206
.get('*', handleRequest);
166207

167-
const sentry = new Toucan({
168-
dsn: env.SENTRY_DSN,
169-
environment: env.SENTRY_ENVIRONMENT,
170-
release: env.SENTRY_RELEASE,
171-
dist: 'cdn-worker',
172-
context: ctx,
173-
request,
174-
requestDataOptions: {
175-
allowedHeaders: [
176-
'user-agent',
177-
'cf-ipcountry',
178-
'accept-encoding',
179-
'accept',
180-
'x-real-ip',
181-
'cf-connecting-ip',
182-
],
183-
allowedSearchParams: /(.*)/,
184-
},
185-
});
186-
187208
try {
188209
return await router.handle(request, sentry.captureException.bind(sentry)).then(response => {
189210
if (response) {
190211
return response;
191212
}
213+
214+
sentry.addBreadcrumb({
215+
message: 'No response from router',
216+
});
217+
192218
return createResponse(analytics, 'Not found', { status: 404 }, 'unknown', request);
193219
});
194220
} catch (error) {

packages/services/cdn-worker/src/key-validation.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import bcrypt from 'bcryptjs';
22
import { Analytics } from './analytics';
33
import { ArtifactStorageReader } from './artifact-storage-reader';
4+
import type { Breadcrumb } from './breadcrumbs';
45
import { decodeCdnAccessTokenSafe, isCDNAccessToken } from './cdn-token';
56

67
export type KeyValidator = (targetId: string, headerKey: string) => Promise<boolean>;
@@ -14,6 +15,8 @@ type CreateKeyValidatorDeps = {
1415
artifactStorageReader: ArtifactStorageReader;
1516
getCache: null | GetCache;
1617
analytics: null | Analytics;
18+
breadcrumb: null | Breadcrumb;
19+
captureException: (error: Error) => void;
1720
};
1821

1922
export const createIsKeyValid =
@@ -223,7 +226,20 @@ async function handleCDNAccessToken(
223226
return withCache(false);
224227
}
225228

226-
const isValid = await bcrypt.compare(decodeResult.token.privateKey, await key.text());
229+
const isValid = await bcrypt
230+
.compare(
231+
decodeResult.token.privateKey,
232+
await key.text().catch(error => {
233+
deps.breadcrumb?.('Failed to read body of key: ' + error.message);
234+
deps.captureException(error);
235+
return Promise.reject(error);
236+
}),
237+
)
238+
.catch(error => {
239+
deps.breadcrumb?.(`Failed to compare keys: ${error.message}`);
240+
deps.captureException(error);
241+
return Promise.reject(error);
242+
});
227243

228244
deps.analytics?.track(
229245
{

0 commit comments

Comments
 (0)