mirror of
https://github.com/graphql-hive/console
synced 2026-05-23 09:08:34 +00:00
Add request path to the cdn metrics (#3310)
This commit is contained in:
parent
f34274641c
commit
0f289e5293
6 changed files with 173 additions and 113 deletions
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
|
|||
): Promise<Response | null> => {
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -135,12 +135,12 @@ const handler: ExportedHandler<Env> = {
|
|||
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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue