diff --git a/packages/services/cdn-worker/src/analytics.ts b/packages/services/cdn-worker/src/analytics.ts index 67fa2dd8f..104307e75 100644 --- a/packages/services/cdn-worker/src/analytics.ts +++ b/packages/services/cdn-worker/src/analytics.ts @@ -58,6 +58,7 @@ type Event = | { type: 'response'; statusCode: number; + requestPath: string; }; export function createAnalytics( @@ -87,12 +88,12 @@ export function createAnalytics( }); case 'r2': return engines.r2.writeDataPoint({ - blobs: [event.action, event.statusCode.toString()], + blobs: [event.action, event.statusCode.toString(), targetId], indexes: [targetId.substring(0, 32)], }); case 'response': return engines.response.writeDataPoint({ - blobs: [event.statusCode.toString()], + blobs: [event.statusCode.toString(), event.requestPath, targetId], indexes: [targetId.substring(0, 32)], }); case 'key-validation': diff --git a/packages/services/cdn-worker/src/artifact-handler.ts b/packages/services/cdn-worker/src/artifact-handler.ts index 7694aa1a0..a9262f834 100644 --- a/packages/services/cdn-worker/src/artifact-handler.ts +++ b/packages/services/cdn-worker/src/artifact-handler.ts @@ -48,7 +48,7 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => { ): Promise => { const headerKey = request.headers.get(authHeaderName); if (headerKey === null) { - return new MissingAuthKeyResponse(analytics); + return new MissingAuthKeyResponse(analytics, request); } const isValid = await deps.isKeyValid(targetId, headerKey); @@ -57,7 +57,7 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => { return null; } - return new InvalidAuthKeyResponse(analytics); + return new InvalidAuthKeyResponse(analytics, request); }; router.get( @@ -77,6 +77,7 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => { status: 404, }, request.params?.targetId ?? 'unknown', + request, ); } @@ -94,6 +95,7 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => { }, }, params.targetId, + request, ); } @@ -133,6 +135,7 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => { 'Something went wrong, really wrong.', { status: 500 }, params.targetId, + request, ) ); } @@ -145,10 +148,11 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => { status: 304, }, params.targetId, + request, ); } if (result.type === 'notFound') { - return createResponse(analytics, 'Not found.', { status: 404 }, params.targetId); + return createResponse(analytics, 'Not found.', { status: 404 }, params.targetId, request); } if (result.type === 'redirect') { return createResponse( @@ -156,6 +160,7 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => { 'Found.', { status: 302, headers: { Location: result.location } }, params.targetId, + request, ); } }, diff --git a/packages/services/cdn-worker/src/errors.ts b/packages/services/cdn-worker/src/errors.ts index e440056ec..bcf62c241 100644 --- a/packages/services/cdn-worker/src/errors.ts +++ b/packages/services/cdn-worker/src/errors.ts @@ -4,7 +4,7 @@ import { Analytics } from './analytics'; const description = `Please refer to the documentation for more details: https://docs.graphql-hive.com/features/registry-usage`; export class MissingTargetIDErrorResponse extends Response { - constructor(analytics: Analytics) { + constructor(analytics: Analytics, request: Request) { super( JSON.stringify({ code: 'MISSING_TARGET_ID', @@ -24,6 +24,7 @@ export class MissingTargetIDErrorResponse extends Response { { type: 'response', statusCode: 400, + requestPath: request.url, }, 'unknown', ); @@ -31,7 +32,7 @@ export class MissingTargetIDErrorResponse extends Response { } export class InvalidArtifactTypeResponse extends Response { - constructor(artifactType: string, analytics: Analytics) { + constructor(artifactType: string, analytics: Analytics, request: Request) { super( JSON.stringify({ code: 'INVALID_ARTIFACT_TYPE', @@ -50,6 +51,7 @@ export class InvalidArtifactTypeResponse extends Response { { type: 'response', statusCode: 400, + requestPath: request.url, }, 'unknown', ); @@ -57,7 +59,7 @@ export class InvalidArtifactTypeResponse extends Response { } export class MissingAuthKeyResponse extends Response { - constructor(analytics: Analytics) { + constructor(analytics: Analytics, request: Request) { super( JSON.stringify({ code: 'MISSING_AUTH_KEY', @@ -76,6 +78,7 @@ export class MissingAuthKeyResponse extends Response { { type: 'response', statusCode: 400, + requestPath: request.url, }, 'unknown', ); @@ -83,7 +86,7 @@ export class MissingAuthKeyResponse extends Response { } export class InvalidAuthKeyResponse extends Response { - constructor(analytics: Analytics) { + constructor(analytics: Analytics, request: Request) { super( JSON.stringify({ code: 'INVALID_AUTH_KEY', @@ -102,6 +105,7 @@ export class InvalidAuthKeyResponse extends Response { { type: 'response', statusCode: 403, + requestPath: request.url, }, 'unknown', ); @@ -109,7 +113,7 @@ export class InvalidAuthKeyResponse extends Response { } export class CDNArtifactNotFound extends Response { - constructor(artifactType: string, targetId: string, analytics: Analytics) { + constructor(artifactType: string, targetId: string, analytics: Analytics, request: Request) { super( JSON.stringify({ code: 'NOT_FOUND', @@ -128,6 +132,7 @@ export class CDNArtifactNotFound extends Response { { type: 'response', statusCode: 404, + requestPath: request.url, }, targetId, ); @@ -135,7 +140,7 @@ export class CDNArtifactNotFound extends Response { } export class InvalidArtifactMatch extends Response { - constructor(artifactType: string, targetId: string, analytics: Analytics) { + constructor(artifactType: string, targetId: string, analytics: Analytics, request: Request) { super( JSON.stringify({ code: 'INVALID_ARTIFACT_MATCH', @@ -154,6 +159,7 @@ export class InvalidArtifactMatch extends Response { { type: 'response', statusCode: 400, + requestPath: request.url, }, targetId, ); @@ -161,7 +167,7 @@ export class InvalidArtifactMatch extends Response { } export class UnexpectedError extends Response { - constructor(analytics: Analytics) { + constructor(analytics: Analytics, request: Request) { super( JSON.stringify({ code: 'UNEXPECTED_ERROR', @@ -180,6 +186,7 @@ export class UnexpectedError extends Response { { type: 'response', statusCode: 500, + requestPath: request.url, }, 'unknown', ); diff --git a/packages/services/cdn-worker/src/handler.ts b/packages/services/cdn-worker/src/handler.ts index 2593728f1..305362b97 100644 --- a/packages/services/cdn-worker/src/handler.ts +++ b/packages/services/cdn-worker/src/handler.ts @@ -29,102 +29,140 @@ type SchemaArtifact = { type ArtifactType = 'schema' | 'supergraph' | 'sdl' | 'metadata' | 'introspection'; const artifactTypes = ['schema', 'supergraph', 'sdl', 'metadata', 'introspection'] as const; -const createArtifactTypesHandlers = ( - analytics: Analytics, -): Record< - ArtifactType, - (targetId: string, artifactType: string, rawValue: string, etag: string) => Response -> => ({ - /** - * Returns SchemaArtifact or SchemaArtifact[], same way as it's stored in the storage - */ - schema: (targetId: string, artifactType: string, rawValue: string, etag: string) => - createResponse( - analytics, - rawValue, - { - status: 200, - headers: { - 'Content-Type': 'application/json', - etag, +function createArtifactTypesHandlers(analytics: Analytics) { + return { + /** + * Returns SchemaArtifact or SchemaArtifact[], same way as it's stored in the storage + */ + schema( + request: Request, + targetId: string, + artifactType: string, + rawValue: string, + etag: string, + ) { + return createResponse( + analytics, + rawValue, + { + status: 200, + headers: { + 'Content-Type': 'application/json', + etag, + }, }, - }, - targetId, - ), - /** - * Returns Federation Supergraph, we store it as-is. - */ - supergraph: (targetId: string, artifactType: string, rawValue: string, etag: string) => - createResponse( - analytics, - rawValue, - { - status: 200, - headers: { - etag, + targetId, + request, + ); + }, + /** + * Returns Federation Supergraph, we store it as-is. + */ + supergraph( + request: Request, + targetId: string, + artifactType: string, + rawValue: string, + etag: string, + ) { + return createResponse( + analytics, + rawValue, + { + status: 200, + headers: { + etag, + }, }, - }, - targetId, - ), - sdl: (targetId: string, artifactType: string, rawValue: string, etag: string) => { - if (rawValue.startsWith('[')) { - return new InvalidArtifactMatch(artifactType, targetId, analytics); - } + targetId, + request, + ); + }, + sdl(request: Request, targetId: string, artifactType: string, rawValue: string, etag: string) { + if (rawValue.startsWith('[')) { + return new InvalidArtifactMatch(artifactType, targetId, analytics, request); + } - const parsed = JSON.parse(rawValue) as SchemaArtifact; + const parsed = JSON.parse(rawValue) as SchemaArtifact; - return createResponse( - analytics, - parsed.sdl, - { - status: 200, - headers: { - etag, + return createResponse( + analytics, + parsed.sdl, + { + status: 200, + headers: { + etag, + }, }, - }, - targetId, - ); - }, - /** - * Returns Metadata same way as it's stored in the storage - */ - metadata: (targetId: string, artifactType: string, rawValue: string, etag: string) => - createResponse( - analytics, - rawValue, - { - status: 200, - headers: { - 'Content-Type': 'application/json', - etag, + targetId, + request, + ); + }, + /** + * Returns Metadata same way as it's stored in the storage + */ + metadata( + request: Request, + targetId: string, + artifactType: string, + rawValue: string, + etag: string, + ) { + return createResponse( + analytics, + rawValue, + { + status: 200, + headers: { + 'Content-Type': 'application/json', + etag, + }, }, - }, - targetId, - ), - introspection: (targetId: string, artifactType: string, rawValue: string, etag: string) => { - if (rawValue.startsWith('[')) { - return new InvalidArtifactMatch(artifactType, targetId, analytics); - } + targetId, + request, + ); + }, + introspection( + request: Request, + targetId: string, + artifactType: string, + rawValue: string, + etag: string, + ) { + if (rawValue.startsWith('[')) { + return new InvalidArtifactMatch(artifactType, targetId, analytics, request); + } - const parsed = JSON.parse(rawValue) as SchemaArtifact; - const rawSdl = parsed.sdl; - const schema = buildSchema(rawSdl); - const introspection = introspectionFromSchema(schema); + const parsed = JSON.parse(rawValue) as SchemaArtifact; + const rawSdl = parsed.sdl; + const schema = buildSchema(rawSdl); + const introspection = introspectionFromSchema(schema); - return createResponse( - analytics, - JSON.stringify(introspection), - { - status: 200, - headers: { - 'Content-Type': 'application/json', - etag, + return createResponse( + analytics, + JSON.stringify(introspection), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + etag, + }, }, - }, - targetId, - ); - }, -}); + targetId, + request, + ); + }, + } satisfies Record< + ArtifactType, + ( + request: Request, + targetId: string, + artifactType: string, + rawValue: string, + etag: string, + ) => Response + >; +} const VALID_ARTIFACT_TYPES = artifactTypes; const AUTH_HEADER_NAME = 'x-hive-cdn-key'; @@ -146,20 +184,20 @@ async function parseIncomingRequest( if (!targetId) { return { - error: new MissingTargetIDErrorResponse(analytics), + error: new MissingTargetIDErrorResponse(analytics, request), }; } const artifactType = (params[1] || 'schema') as ArtifactType; if (!VALID_ARTIFACT_TYPES.includes(artifactType)) { - return { error: new InvalidArtifactTypeResponse(artifactType, analytics) }; + return { error: new InvalidArtifactTypeResponse(artifactType, analytics, request) }; } const headerKey = request.headers.get(AUTH_HEADER_NAME); if (!headerKey) { - return { error: new MissingAuthKeyResponse(analytics) }; + return { error: new MissingAuthKeyResponse(analytics, request) }; } try { @@ -167,7 +205,7 @@ async function parseIncomingRequest( if (!keyValid) { return { - error: new InvalidAuthKeyResponse(analytics), + error: new InvalidAuthKeyResponse(analytics, request), }; } @@ -182,7 +220,7 @@ async function parseIncomingRequest( } catch (e) { console.warn(`Failed to validate key for ${targetId}, error:`, e); return { - error: new InvalidAuthKeyResponse(analytics), + error: new InvalidAuthKeyResponse(analytics, request), }; } } @@ -231,20 +269,26 @@ export const createRequestHandler = (deps: RequestHandlerDependencies) => { const ifNoneMatch = request.headers.get('if-none-match'); if (ifNoneMatch && ifNoneMatch === etag) { - return createResponse(analytics, null, { status: 304 }, targetId); + return createResponse(analytics, null, { status: 304 }, targetId, request); } switch (artifactType) { case 'schema': - return artifactTypesHandlers.schema(targetId, artifactType, rawValue, etag); + return artifactTypesHandlers.schema(request, targetId, artifactType, rawValue, etag); case 'supergraph': - return artifactTypesHandlers.supergraph(targetId, artifactType, rawValue, etag); + return artifactTypesHandlers.supergraph(request, targetId, artifactType, rawValue, etag); case 'sdl': - return artifactTypesHandlers.sdl(targetId, artifactType, rawValue, etag); + return artifactTypesHandlers.sdl(request, targetId, artifactType, rawValue, etag); case 'introspection': - return artifactTypesHandlers.introspection(targetId, artifactType, rawValue, etag); + return artifactTypesHandlers.introspection( + request, + targetId, + artifactType, + rawValue, + etag, + ); case 'metadata': - return artifactTypesHandlers.metadata(targetId, artifactType, rawValue, etag); + return artifactTypesHandlers.metadata(request, targetId, artifactType, rawValue, etag); default: return createResponse( analytics, @@ -253,13 +297,14 @@ export const createRequestHandler = (deps: RequestHandlerDependencies) => { status: 500, }, targetId, + request, ); } } else { console.log( `CDN Artifact not found for targetId=${targetId}, artifactType=${artifactType}, storageKeyType=${storageKeyType}`, ); - return new CDNArtifactNotFound(artifactType, targetId, analytics); + return new CDNArtifactNotFound(artifactType, targetId, analytics, request); } }; }; diff --git a/packages/services/cdn-worker/src/index.ts b/packages/services/cdn-worker/src/index.ts index f7ac932dd..7cca3b9e5 100644 --- a/packages/services/cdn-worker/src/index.ts +++ b/packages/services/cdn-worker/src/index.ts @@ -135,12 +135,12 @@ const handler: ExportedHandler = { if (response) { return response; } - return createResponse(analytics, 'Not found', { status: 404 }, 'unknown'); + return createResponse(analytics, 'Not found', { status: 404 }, 'unknown', request); }); } catch (error) { console.error(error); sentry.captureException(error); - return new UnexpectedError(analytics); + return new UnexpectedError(analytics, request); } }, }; diff --git a/packages/services/cdn-worker/src/tracked-response.ts b/packages/services/cdn-worker/src/tracked-response.ts index 8283b644d..398564609 100644 --- a/packages/services/cdn-worker/src/tracked-response.ts +++ b/packages/services/cdn-worker/src/tracked-response.ts @@ -8,11 +8,13 @@ export function createResponse( body: BodyInit | null, init: ResponseInit, targetId: string, + request: Request, ) { analytics.track( { type: 'response', statusCode: init.status ?? 999 /* indicates unknown status code, for some reason... */, + requestPath: request.url, }, targetId, );