mirror of
https://github.com/graphql-hive/console
synced 2026-05-23 17:18:23 +00:00
Track responses and R2 calls (#2815)
This commit is contained in:
parent
86d884069e
commit
c4d09cce6f
9 changed files with 224 additions and 55 deletions
|
|
@ -49,6 +49,15 @@ type Event =
|
|||
| {
|
||||
type: 'error';
|
||||
value: [string, string] | [string];
|
||||
}
|
||||
| {
|
||||
type: 'r2';
|
||||
action: 'HEAD artifact' | 'GET cdn-legacy-keys' | 'GET cdn-access-token';
|
||||
statusCode: number;
|
||||
}
|
||||
| {
|
||||
type: 'response';
|
||||
statusCode: number;
|
||||
};
|
||||
|
||||
export function createAnalytics(
|
||||
|
|
@ -68,7 +77,7 @@ export function createAnalytics(
|
|||
case 'artifact':
|
||||
return engines.usage.writeDataPoint({
|
||||
blobs: [event.version, event.value, targetId],
|
||||
indexes: [targetId.substr(0, 32)],
|
||||
indexes: [targetId.substring(0, 32)],
|
||||
});
|
||||
case 'error':
|
||||
return engines.error.writeDataPoint({
|
||||
|
|
@ -83,7 +92,7 @@ export function createAnalytics(
|
|||
event.value.version,
|
||||
event.value.isValid ? 'valid' : 'invalid',
|
||||
],
|
||||
indexes: [targetId.substr(0, 32)],
|
||||
indexes: [targetId.substring(0, 32)],
|
||||
});
|
||||
case 'cache-write':
|
||||
return engines.keyValidation.writeDataPoint({
|
||||
|
|
@ -92,12 +101,12 @@ export function createAnalytics(
|
|||
event.value.version,
|
||||
event.value.isValid ? 'valid' : 'invalid',
|
||||
],
|
||||
indexes: [targetId.substr(0, 32)],
|
||||
indexes: [targetId.substring(0, 32)],
|
||||
});
|
||||
case 's3-key-validation':
|
||||
return engines.keyValidation.writeDataPoint({
|
||||
blobs: ['s3-key-validation', event.value.version, event.value.status],
|
||||
indexes: [targetId.substr(0, 32)],
|
||||
indexes: [targetId.substring(0, 32)],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import itty from 'itty-router';
|
||||
import zod from 'zod';
|
||||
import { createFetch, type Request } from '@whatwg-node/fetch';
|
||||
import { type Request } from '@whatwg-node/fetch';
|
||||
import { createAnalytics, type Analytics } from './analytics';
|
||||
import { type ArtifactsType } from './artifact-storage-reader';
|
||||
import { InvalidAuthKeyResponse, MissingAuthKeyResponse } from './errors';
|
||||
import type { KeyValidator } from './key-validation';
|
||||
|
||||
const { Response } = createFetch({ useNodeFetch: true });
|
||||
import { createResponse } from './tracked-response';
|
||||
|
||||
type ArtifactRequestHandler = {
|
||||
getArtifactAction: (
|
||||
|
|
@ -67,19 +66,35 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
|
|||
const parseResult = ParamsModel.safeParse(request.params);
|
||||
|
||||
if (parseResult.success === false) {
|
||||
return new Response('Not found.', { status: 404 });
|
||||
analytics.track(
|
||||
{ type: 'error', value: ['invalid-params'] },
|
||||
request.params?.targetId ?? 'unknown',
|
||||
);
|
||||
return createResponse(
|
||||
analytics,
|
||||
'Not found.',
|
||||
{
|
||||
status: 404,
|
||||
},
|
||||
request.params?.targetId ?? 'unknown',
|
||||
);
|
||||
}
|
||||
|
||||
const params = parseResult.data;
|
||||
|
||||
/** Legacy handling for old client SDK versions. */
|
||||
if (params.artifactType === 'schema') {
|
||||
return new Response('Found.', {
|
||||
status: 301,
|
||||
headers: {
|
||||
Location: request.url.replace('/schema', '/services'),
|
||||
return createResponse(
|
||||
analytics,
|
||||
'Found.',
|
||||
{
|
||||
status: 301,
|
||||
headers: {
|
||||
Location: request.url.replace('/schema', '/services'),
|
||||
},
|
||||
},
|
||||
});
|
||||
params.targetId,
|
||||
);
|
||||
}
|
||||
|
||||
const maybeResponse = await authenticate(request, params.targetId);
|
||||
|
|
@ -113,20 +128,35 @@ export const createArtifactRequestHandler = (deps: ArtifactRequestHandler) => {
|
|||
if (!result) {
|
||||
return (
|
||||
deps.fallback?.(request, params) ??
|
||||
new Response('Something went wrong, really wrong.', { status: 500 })
|
||||
createResponse(
|
||||
analytics,
|
||||
'Something went wrong, really wrong.',
|
||||
{ status: 500 },
|
||||
params.targetId,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (result.type === 'notModified') {
|
||||
return new Response('', {
|
||||
status: 304,
|
||||
});
|
||||
return createResponse(
|
||||
analytics,
|
||||
'',
|
||||
{
|
||||
status: 304,
|
||||
},
|
||||
params.targetId,
|
||||
);
|
||||
}
|
||||
if (result.type === 'notFound') {
|
||||
return new Response('Not found.', { status: 404 });
|
||||
return createResponse(analytics, 'Not found.', { status: 404 }, params.targetId);
|
||||
}
|
||||
if (result.type === 'redirect') {
|
||||
return new Response('Found.', { status: 302, headers: { Location: result.location } });
|
||||
return createResponse(
|
||||
analytics,
|
||||
'Found.',
|
||||
{ status: 302, headers: { Location: result.location } },
|
||||
params.targetId,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { Analytics } from './analytics';
|
||||
import { AwsClient } from './aws';
|
||||
|
||||
const presignedUrlExpirationSeconds = 60;
|
||||
|
|
@ -23,6 +24,7 @@ export class ArtifactStorageReader {
|
|||
},
|
||||
/** The public URL in case the public S3 endpoint differs from the internal S3 endpoint. E.g. within a docker network. */
|
||||
publicUrl: string | null,
|
||||
private analytics: Analytics | null,
|
||||
) {
|
||||
this.publicUrl = publicUrl ? new URL(publicUrl) : null;
|
||||
}
|
||||
|
|
@ -72,6 +74,14 @@ export class ArtifactStorageReader {
|
|||
},
|
||||
},
|
||||
);
|
||||
this.analytics?.track(
|
||||
{
|
||||
type: 'r2',
|
||||
statusCode: response.status,
|
||||
action: 'HEAD artifact',
|
||||
},
|
||||
targetId,
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
if (etagValue && response.headers.get('etag') === etagValue) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,13 @@ export class MissingTargetIDErrorResponse extends Response {
|
|||
);
|
||||
|
||||
analytics.track({ type: 'error', value: ['missing_target_id'] }, 'unknown');
|
||||
analytics.track(
|
||||
{
|
||||
type: 'response',
|
||||
statusCode: 400,
|
||||
},
|
||||
'unknown',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +46,13 @@ export class InvalidArtifactTypeResponse extends Response {
|
|||
},
|
||||
);
|
||||
analytics.track({ type: 'error', value: ['invalid_artifact_type', artifactType] }, 'unknown');
|
||||
analytics.track(
|
||||
{
|
||||
type: 'response',
|
||||
statusCode: 400,
|
||||
},
|
||||
'unknown',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +72,13 @@ export class MissingAuthKeyResponse extends Response {
|
|||
},
|
||||
);
|
||||
analytics.track({ type: 'error', value: ['missing_auth_key'] }, 'unknown');
|
||||
analytics.track(
|
||||
{
|
||||
type: 'response',
|
||||
statusCode: 400,
|
||||
},
|
||||
'unknown',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +98,13 @@ export class InvalidAuthKeyResponse extends Response {
|
|||
},
|
||||
);
|
||||
analytics.track({ type: 'error', value: ['invalid_auth_key'] }, 'unknown');
|
||||
analytics.track(
|
||||
{
|
||||
type: 'response',
|
||||
statusCode: 403,
|
||||
},
|
||||
'unknown',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +124,13 @@ export class CDNArtifactNotFound extends Response {
|
|||
},
|
||||
);
|
||||
analytics.track({ type: 'error', value: ['artifact_not_found', artifactType] }, targetId);
|
||||
analytics.track(
|
||||
{
|
||||
type: 'response',
|
||||
statusCode: 404,
|
||||
},
|
||||
targetId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +150,13 @@ export class InvalidArtifactMatch extends Response {
|
|||
},
|
||||
);
|
||||
analytics.track({ type: 'error', value: ['invalid_artifact_match', artifactType] }, targetId);
|
||||
analytics.track(
|
||||
{
|
||||
type: 'response',
|
||||
statusCode: 400,
|
||||
},
|
||||
targetId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,5 +176,12 @@ export class UnexpectedError extends Response {
|
|||
},
|
||||
);
|
||||
analytics.track({ type: 'error', value: ['unexpected_error'] }, 'unknown');
|
||||
analytics.track(
|
||||
{
|
||||
type: 'response',
|
||||
statusCode: 500,
|
||||
},
|
||||
'unknown',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
MissingTargetIDErrorResponse,
|
||||
} from './errors';
|
||||
import type { KeyValidator } from './key-validation';
|
||||
import { createResponse } from './tracked-response';
|
||||
|
||||
async function createETag(value: string) {
|
||||
const myText = new TextEncoder().encode(value);
|
||||
|
|
@ -38,23 +39,33 @@ const createArtifactTypesHandlers = (
|
|||
* Returns SchemaArtifact or SchemaArtifact[], same way as it's stored in the storage
|
||||
*/
|
||||
schema: (targetId: string, artifactType: string, rawValue: string, etag: string) =>
|
||||
new Response(rawValue, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
etag,
|
||||
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) =>
|
||||
new Response(rawValue, {
|
||||
status: 200,
|
||||
headers: {
|
||||
etag,
|
||||
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);
|
||||
|
|
@ -62,24 +73,34 @@ const createArtifactTypesHandlers = (
|
|||
|
||||
const parsed = JSON.parse(rawValue) as SchemaArtifact;
|
||||
|
||||
return new Response(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) =>
|
||||
new Response(rawValue, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
etag,
|
||||
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);
|
||||
|
|
@ -90,13 +111,18 @@ const createArtifactTypesHandlers = (
|
|||
const schema = buildSchema(rawSdl);
|
||||
const introspection = introspectionFromSchema(schema);
|
||||
|
||||
return new Response(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,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -205,7 +231,7 @@ export const createRequestHandler = (deps: RequestHandlerDependencies) => {
|
|||
const ifNoneMatch = request.headers.get('if-none-match');
|
||||
|
||||
if (ifNoneMatch && ifNoneMatch === etag) {
|
||||
return new Response(null, { status: 304 });
|
||||
return createResponse(analytics, null, { status: 304 }, targetId);
|
||||
}
|
||||
|
||||
switch (artifactType) {
|
||||
|
|
@ -220,9 +246,14 @@ export const createRequestHandler = (deps: RequestHandlerDependencies) => {
|
|||
case 'metadata':
|
||||
return artifactTypesHandlers.metadata(targetId, artifactType, rawValue, etag);
|
||||
default:
|
||||
return new Response(null, {
|
||||
status: 500,
|
||||
});
|
||||
return createResponse(
|
||||
analytics,
|
||||
null,
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
targetId,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { AwsClient } from './aws';
|
|||
import { UnexpectedError } from './errors';
|
||||
import { createRequestHandler } from './handler';
|
||||
import { createIsKeyValid } from './key-validation';
|
||||
import { createResponse } from './tracked-response';
|
||||
|
||||
type Env = {
|
||||
S3_ENDPOINT: string;
|
||||
|
|
@ -45,14 +46,14 @@ const handler: ExportedHandler<Env> = {
|
|||
endpoint: env.S3_ENDPOINT,
|
||||
};
|
||||
|
||||
const artifactStorageReader = new ArtifactStorageReader(s3, null);
|
||||
|
||||
const analytics = createAnalytics({
|
||||
usage: env.USAGE_ANALYTICS,
|
||||
error: env.ERROR_ANALYTICS,
|
||||
keyValidation: env.KEY_VALIDATION_ANALYTICS,
|
||||
});
|
||||
|
||||
const artifactStorageReader = new ArtifactStorageReader(s3, null, analytics);
|
||||
|
||||
const isKeyValid = createIsKeyValid({
|
||||
waitUntil: p => ctx.waitUntil(p),
|
||||
getCache: () => caches.open('artifacts-auth'),
|
||||
|
|
@ -130,7 +131,7 @@ const handler: ExportedHandler<Env> = {
|
|||
if (response) {
|
||||
return response;
|
||||
}
|
||||
return new Response('Not found', { status: 404 });
|
||||
return createResponse(analytics, 'Not found', { status: 404 }, 'unknown');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
|
|
|||
|
|
@ -126,6 +126,15 @@ const handleLegacyCDNAccessToken = async (args: {
|
|||
},
|
||||
);
|
||||
|
||||
args.analytics?.track(
|
||||
{
|
||||
type: 'r2',
|
||||
statusCode: key.status,
|
||||
action: 'GET cdn-legacy-keys',
|
||||
},
|
||||
args.targetId,
|
||||
);
|
||||
|
||||
if (key.status !== 200) {
|
||||
return withCache(false);
|
||||
}
|
||||
|
|
@ -238,6 +247,15 @@ async function handleCDNAccessToken(
|
|||
},
|
||||
);
|
||||
|
||||
deps.analytics?.track(
|
||||
{
|
||||
type: 'r2',
|
||||
statusCode: key.status,
|
||||
action: 'GET cdn-access-token',
|
||||
},
|
||||
targetId,
|
||||
);
|
||||
|
||||
if (key.status !== 200) {
|
||||
return withCache(false);
|
||||
}
|
||||
|
|
|
|||
21
packages/services/cdn-worker/src/tracked-response.ts
Normal file
21
packages/services/cdn-worker/src/tracked-response.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { createFetch } from '@whatwg-node/fetch';
|
||||
import type { Analytics } from './analytics';
|
||||
|
||||
const { Response } = createFetch({ useNodeFetch: true });
|
||||
|
||||
export function createResponse(
|
||||
analytics: Analytics,
|
||||
body: BodyInit | null,
|
||||
init: ResponseInit,
|
||||
targetId: string,
|
||||
) {
|
||||
analytics.track(
|
||||
{
|
||||
type: 'response',
|
||||
statusCode: init.status ?? 999 /* indicates unknown status code, for some reason... */,
|
||||
},
|
||||
targetId,
|
||||
);
|
||||
|
||||
return new Response(body, init);
|
||||
}
|
||||
|
|
@ -380,7 +380,7 @@ export async function main() {
|
|||
bucketName: env.s3.bucketName,
|
||||
};
|
||||
|
||||
const artifactStorageReader = new ArtifactStorageReader(s3, env.s3.publicUrl);
|
||||
const artifactStorageReader = new ArtifactStorageReader(s3, env.s3.publicUrl, null);
|
||||
|
||||
const artifactHandler = createArtifactRequestHandler({
|
||||
isKeyValid: createIsKeyValid({ s3, analytics: null, getCache: null, waitUntil: null }),
|
||||
|
|
|
|||
Loading…
Reference in a new issue