Add telemetry diagnostics route
This commit is contained in:
@@ -51,6 +51,7 @@ export interface Env {
|
|||||||
ELY_STORAGE: ElyR2Bucket;
|
ELY_STORAGE: ElyR2Bucket;
|
||||||
ELY_RATE_LIMITER: ElyRateLimit;
|
ELY_RATE_LIMITER: ElyRateLimit;
|
||||||
ELY_API_AUDIT: ElyAnalyticsDataset;
|
ELY_API_AUDIT: ElyAnalyticsDataset;
|
||||||
|
ELY_DIAGNOSTICS: ElyAnalyticsDataset;
|
||||||
ELY_ENVIRONMENT: string;
|
ELY_ENVIRONMENT: string;
|
||||||
ELY_AUTH_BASE_URL: string;
|
ELY_AUTH_BASE_URL: string;
|
||||||
ELY_AUTH_SECRET: string;
|
ELY_AUTH_SECRET: string;
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ import {
|
|||||||
publicSigningKeysKvKey,
|
publicSigningKeysKvKey,
|
||||||
} from "./signing_keys.js";
|
} from "./signing_keys.js";
|
||||||
import { handleSyncRoute } from "./sync_routes.js";
|
import { handleSyncRoute } from "./sync_routes.js";
|
||||||
|
import {
|
||||||
|
TelemetrySchemaError,
|
||||||
|
telemetryEventAcceptedDocument,
|
||||||
|
} from "./telemetry.js";
|
||||||
import { jsonResponse } from "./responses.js";
|
import { jsonResponse } from "./responses.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -199,6 +203,32 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
|
|||||||
handleReleaseSignature(env, url),
|
handleReleaseSignature(env, url),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (url.pathname === "/api/telemetry/events") {
|
||||||
|
return withAuthenticatedApiControls(
|
||||||
|
request,
|
||||||
|
env,
|
||||||
|
"telemetry.events",
|
||||||
|
["POST"],
|
||||||
|
async (context) => {
|
||||||
|
try {
|
||||||
|
return jsonResponse(
|
||||||
|
await telemetryEventAcceptedDocument(request, env, context),
|
||||||
|
202,
|
||||||
|
{ "Cache-Control": "no-store" },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof TelemetrySchemaError) {
|
||||||
|
return jsonResponse(
|
||||||
|
{ error: error.code },
|
||||||
|
400,
|
||||||
|
{ "Cache-Control": "no-store" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return jsonResponse({ error: "not_found" }, 404);
|
return jsonResponse({ error: "not_found" }, 404);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import type { AuthContext } from "./auth.js";
|
||||||
|
import type { Env } from "./bindings.js";
|
||||||
|
|
||||||
|
const EVENT_TYPES = new Set([
|
||||||
|
"app_startup",
|
||||||
|
"app_crash",
|
||||||
|
"webview_crash",
|
||||||
|
"sync_error",
|
||||||
|
"plugin_crash",
|
||||||
|
"update_result",
|
||||||
|
]);
|
||||||
|
const OUTCOMES = new Set(["success", "failure"]);
|
||||||
|
const PLATFORMS = new Set(["macos", "windows", "linux"]);
|
||||||
|
const ALLOWED_FIELDS = new Set([
|
||||||
|
"version",
|
||||||
|
"event_type",
|
||||||
|
"occurred_at",
|
||||||
|
"app_version",
|
||||||
|
"platform",
|
||||||
|
"outcome",
|
||||||
|
"error_code",
|
||||||
|
"component",
|
||||||
|
"crash_kind",
|
||||||
|
"plugin_id",
|
||||||
|
]);
|
||||||
|
const SENSITIVE_FIELDS = new Set([
|
||||||
|
"url",
|
||||||
|
"loaded_url",
|
||||||
|
"loadedurl",
|
||||||
|
"page_url",
|
||||||
|
"pageurl",
|
||||||
|
"title",
|
||||||
|
"page_title",
|
||||||
|
"pagetitle",
|
||||||
|
"search_term",
|
||||||
|
"searchterm",
|
||||||
|
"query",
|
||||||
|
"bookmark",
|
||||||
|
"bookmark_content",
|
||||||
|
"bookmarkcontent",
|
||||||
|
"history",
|
||||||
|
"history_content",
|
||||||
|
"historycontent",
|
||||||
|
"note",
|
||||||
|
"notes",
|
||||||
|
"notes_content",
|
||||||
|
"notescontent",
|
||||||
|
"cookie",
|
||||||
|
"cookies",
|
||||||
|
"form",
|
||||||
|
"form_input",
|
||||||
|
"forminput",
|
||||||
|
"input",
|
||||||
|
"field_value",
|
||||||
|
"fieldvalue",
|
||||||
|
"payload",
|
||||||
|
"body",
|
||||||
|
"page_text",
|
||||||
|
"pagetext",
|
||||||
|
]);
|
||||||
|
const APP_VERSION_PATTERN = /^[0-9A-Za-z][0-9A-Za-z._+-]{0,63}$/;
|
||||||
|
const CODE_PATTERN = /^[0-9A-Za-z][0-9A-Za-z._:-]{0,79}$/;
|
||||||
|
|
||||||
|
type TelemetryRequestBody = Record<string, unknown>;
|
||||||
|
type TelemetryEventType =
|
||||||
|
| "app_startup"
|
||||||
|
| "app_crash"
|
||||||
|
| "webview_crash"
|
||||||
|
| "sync_error"
|
||||||
|
| "plugin_crash"
|
||||||
|
| "update_result";
|
||||||
|
type TelemetryOutcome = "success" | "failure";
|
||||||
|
type TelemetryPlatform = "macos" | "windows" | "linux";
|
||||||
|
|
||||||
|
export interface TelemetryAcceptedDocument {
|
||||||
|
version: 1;
|
||||||
|
accepted: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TelemetryEvent {
|
||||||
|
eventType: TelemetryEventType;
|
||||||
|
occurredAt: number;
|
||||||
|
appVersion: string;
|
||||||
|
platform: TelemetryPlatform;
|
||||||
|
outcome?: TelemetryOutcome;
|
||||||
|
errorCode?: string;
|
||||||
|
component?: string;
|
||||||
|
crashKind?: string;
|
||||||
|
pluginId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TelemetrySchemaError extends Error {
|
||||||
|
constructor(readonly code: string) {
|
||||||
|
super(code);
|
||||||
|
this.name = "TelemetrySchemaError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function telemetryEventAcceptedDocument(
|
||||||
|
request: Request,
|
||||||
|
env: Env,
|
||||||
|
context: AuthContext,
|
||||||
|
): Promise<TelemetryAcceptedDocument> {
|
||||||
|
const event = await telemetryEvent(request);
|
||||||
|
env.ELY_DIAGNOSTICS.writeDataPoint({
|
||||||
|
indexes: [env.ELY_ENVIRONMENT],
|
||||||
|
blobs: [
|
||||||
|
event.eventType,
|
||||||
|
event.appVersion,
|
||||||
|
event.platform,
|
||||||
|
event.outcome ?? "",
|
||||||
|
event.errorCode ?? "",
|
||||||
|
event.component ?? "",
|
||||||
|
event.crashKind ?? "",
|
||||||
|
event.pluginId ?? "",
|
||||||
|
context.userId,
|
||||||
|
context.deviceId ?? "",
|
||||||
|
],
|
||||||
|
doubles: [event.occurredAt, Date.now()],
|
||||||
|
});
|
||||||
|
return { version: 1, accepted: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function telemetryEvent(request: Request): Promise<TelemetryEvent> {
|
||||||
|
const body = await requestBody(request);
|
||||||
|
assertNoSensitiveFields(body);
|
||||||
|
assertOnlyFields(body);
|
||||||
|
if (body.version !== 1) {
|
||||||
|
throw new TelemetrySchemaError("telemetry_version_invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
const event: TelemetryEvent = {
|
||||||
|
eventType: eventType(body.event_type),
|
||||||
|
occurredAt: integer(body.occurred_at, "telemetry_occurred_at_invalid"),
|
||||||
|
appVersion: appVersion(body.app_version),
|
||||||
|
platform: platform(body.platform),
|
||||||
|
};
|
||||||
|
assignOptionalString(event, "outcome", outcome(body.outcome));
|
||||||
|
assignOptionalString(event, "errorCode", code(body.error_code, "telemetry_error_code_invalid"));
|
||||||
|
assignOptionalString(event, "component", code(body.component, "telemetry_component_invalid"));
|
||||||
|
assignOptionalString(event, "crashKind", code(body.crash_kind, "telemetry_crash_kind_invalid"));
|
||||||
|
assignOptionalString(event, "pluginId", code(body.plugin_id, "telemetry_plugin_id_invalid"));
|
||||||
|
assertRequiredEventFields(event);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestBody(request: Request): Promise<TelemetryRequestBody> {
|
||||||
|
let value: unknown;
|
||||||
|
try {
|
||||||
|
value = await request.json();
|
||||||
|
} catch {
|
||||||
|
throw new TelemetrySchemaError("telemetry_json_invalid");
|
||||||
|
}
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
throw new TelemetrySchemaError("telemetry_body_invalid");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNoSensitiveFields(value: unknown): void {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
assertNoSensitiveFields(item);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [field, fieldValue] of Object.entries(value)) {
|
||||||
|
if (SENSITIVE_FIELDS.has(field.toLowerCase())) {
|
||||||
|
throw new TelemetrySchemaError("telemetry_sensitive_field");
|
||||||
|
}
|
||||||
|
assertNoSensitiveFields(fieldValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertOnlyFields(value: TelemetryRequestBody): void {
|
||||||
|
for (const field of Object.keys(value)) {
|
||||||
|
if (!ALLOWED_FIELDS.has(field)) {
|
||||||
|
throw new TelemetrySchemaError("telemetry_unknown_field");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventType(value: unknown): TelemetryEventType {
|
||||||
|
if (typeof value !== "string" || !EVENT_TYPES.has(value)) {
|
||||||
|
throw new TelemetrySchemaError("telemetry_event_type_invalid");
|
||||||
|
}
|
||||||
|
return value as TelemetryEventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function platform(value: unknown): TelemetryPlatform {
|
||||||
|
if (typeof value !== "string" || !PLATFORMS.has(value)) {
|
||||||
|
throw new TelemetrySchemaError("telemetry_platform_invalid");
|
||||||
|
}
|
||||||
|
return value as TelemetryPlatform;
|
||||||
|
}
|
||||||
|
|
||||||
|
function outcome(value: unknown): TelemetryOutcome | undefined {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (typeof value !== "string" || !OUTCOMES.has(value)) {
|
||||||
|
throw new TelemetrySchemaError("telemetry_outcome_invalid");
|
||||||
|
}
|
||||||
|
return value as TelemetryOutcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appVersion(value: unknown): string {
|
||||||
|
if (typeof value !== "string" || !APP_VERSION_PATTERN.test(value)) {
|
||||||
|
throw new TelemetrySchemaError("telemetry_app_version_invalid");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function code(value: unknown, errorCode: string): string | undefined {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (typeof value !== "string" || !CODE_PATTERN.test(value)) {
|
||||||
|
throw new TelemetrySchemaError(errorCode);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function integer(value: unknown, errorCode: string): number {
|
||||||
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
||||||
|
throw new TelemetrySchemaError(errorCode);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignOptionalString<T extends object, K extends keyof T>(
|
||||||
|
target: T,
|
||||||
|
field: K,
|
||||||
|
value: T[K] | undefined,
|
||||||
|
): void {
|
||||||
|
if (value !== undefined) {
|
||||||
|
target[field] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertRequiredEventFields(event: TelemetryEvent): void {
|
||||||
|
if ((event.eventType === "app_startup" || event.eventType === "update_result")
|
||||||
|
&& event.outcome === undefined) {
|
||||||
|
throw new TelemetrySchemaError("telemetry_outcome_required");
|
||||||
|
}
|
||||||
|
if (event.eventType === "sync_error" && event.errorCode === undefined) {
|
||||||
|
throw new TelemetrySchemaError("telemetry_error_code_required");
|
||||||
|
}
|
||||||
|
if (event.eventType === "webview_crash" && event.crashKind === undefined) {
|
||||||
|
throw new TelemetrySchemaError("telemetry_crash_kind_required");
|
||||||
|
}
|
||||||
|
if (event.eventType === "plugin_crash" && event.pluginId === undefined) {
|
||||||
|
throw new TelemetrySchemaError("telemetry_plugin_id_required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is TelemetryRequestBody {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
@@ -334,6 +334,9 @@ function testEnv(options: TestEnvOptions = {}): Env {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ELY_DIAGNOSTICS: {
|
||||||
|
writeDataPoint(): void {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const PUBLIC_KEY = "a".repeat(64);
|
|||||||
|
|
||||||
export interface TestEnvOptions {
|
export interface TestEnvOptions {
|
||||||
auditEvents?: ElyAnalyticsDataPoint[];
|
auditEvents?: ElyAnalyticsDataPoint[];
|
||||||
|
diagnosticEvents?: ElyAnalyticsDataPoint[];
|
||||||
d1?: RecordedD1Database;
|
d1?: RecordedD1Database;
|
||||||
kvEntries?: [string, string][];
|
kvEntries?: [string, string][];
|
||||||
kvReads?: string[];
|
kvReads?: string[];
|
||||||
@@ -68,6 +69,13 @@ export function testEnv(options: TestEnvOptions): Env {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ELY_DIAGNOSTICS: {
|
||||||
|
writeDataPoint(event?: ElyAnalyticsDataPoint): void {
|
||||||
|
if (event !== undefined) {
|
||||||
|
options.diagnosticEvents?.push(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -369,6 +369,9 @@ function testEnv(
|
|||||||
ELY_API_AUDIT: {
|
ELY_API_AUDIT: {
|
||||||
writeDataPoint(): void {},
|
writeDataPoint(): void {},
|
||||||
},
|
},
|
||||||
|
ELY_DIAGNOSTICS: {
|
||||||
|
writeDataPoint(): void {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
|
||||||
|
import type { ElyAnalyticsDataPoint } from "../src/bindings.js";
|
||||||
|
import { authSessionCacheKvKey, authTokenHash } from "../src/auth.js";
|
||||||
|
import { handleRequest } from "../src/index.js";
|
||||||
|
import { ACCESS_TOKEN, sessionDocument, testEnv } from "./devices_test_support.js";
|
||||||
|
|
||||||
|
const DEVICE_ID = "device-01";
|
||||||
|
|
||||||
|
describe("telemetry routes", () => {
|
||||||
|
it("records a minimized authenticated diagnostic event", async () => {
|
||||||
|
const tokenHash = await authTokenHash(ACCESS_TOKEN);
|
||||||
|
const auditEvents: ElyAnalyticsDataPoint[] = [];
|
||||||
|
const diagnosticEvents: ElyAnalyticsDataPoint[] = [];
|
||||||
|
const response = await handleRequest(
|
||||||
|
telemetryRequest({
|
||||||
|
version: 1,
|
||||||
|
event_type: "sync_error",
|
||||||
|
occurred_at: 1_780_000_000,
|
||||||
|
app_version: "0.1.0",
|
||||||
|
platform: "macos",
|
||||||
|
error_code: "sync.pull.5xx",
|
||||||
|
component: "sync",
|
||||||
|
}),
|
||||||
|
testEnv({
|
||||||
|
auditEvents,
|
||||||
|
diagnosticEvents,
|
||||||
|
kvEntries: [[authSessionCacheKvKey("local", tokenHash), sessionDocument(DEVICE_ID)]],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.status, 202);
|
||||||
|
assert.equal(response.headers.get("cache-control"), "no-store");
|
||||||
|
assert.deepEqual(await response.json(), { version: 1, accepted: true });
|
||||||
|
assert.equal(diagnosticEvents.length, 1);
|
||||||
|
assert.deepEqual(diagnosticEvents[0]?.indexes, ["local"]);
|
||||||
|
assert.deepEqual(diagnosticEvents[0]?.blobs, [
|
||||||
|
"sync_error",
|
||||||
|
"0.1.0",
|
||||||
|
"macos",
|
||||||
|
"",
|
||||||
|
"sync.pull.5xx",
|
||||||
|
"sync",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"user-01",
|
||||||
|
DEVICE_ID,
|
||||||
|
]);
|
||||||
|
assert.equal(diagnosticEvents[0]?.doubles?.[0], 1_780_000_000);
|
||||||
|
assert.ok((diagnosticEvents[0]?.doubles?.[1] ?? 0) > 0);
|
||||||
|
assert.deepEqual(auditEvents[0]?.blobs?.slice(0, 4), [
|
||||||
|
"telemetry.events",
|
||||||
|
"POST",
|
||||||
|
"/api/telemetry/events",
|
||||||
|
"handled",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects diagnostic events containing sensitive fields", async () => {
|
||||||
|
const tokenHash = await authTokenHash(ACCESS_TOKEN);
|
||||||
|
const diagnosticEvents: ElyAnalyticsDataPoint[] = [];
|
||||||
|
const response = await handleRequest(
|
||||||
|
telemetryRequest({
|
||||||
|
version: 1,
|
||||||
|
event_type: "app_startup",
|
||||||
|
occurred_at: 1_780_000_000,
|
||||||
|
app_version: "0.1.0",
|
||||||
|
platform: "macos",
|
||||||
|
outcome: "success",
|
||||||
|
url: "https://example.test/private?q=token",
|
||||||
|
}),
|
||||||
|
testEnv({
|
||||||
|
diagnosticEvents,
|
||||||
|
kvEntries: [[authSessionCacheKvKey("local", tokenHash), sessionDocument(DEVICE_ID)]],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.status, 400);
|
||||||
|
assert.deepEqual(await response.json(), { error: "telemetry_sensitive_field" });
|
||||||
|
assert.equal(diagnosticEvents.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects unknown diagnostic fields", async () => {
|
||||||
|
const tokenHash = await authTokenHash(ACCESS_TOKEN);
|
||||||
|
const diagnosticEvents: ElyAnalyticsDataPoint[] = [];
|
||||||
|
const response = await handleRequest(
|
||||||
|
telemetryRequest({
|
||||||
|
version: 1,
|
||||||
|
event_type: "update_result",
|
||||||
|
occurred_at: 1_780_000_000,
|
||||||
|
app_version: "0.1.0",
|
||||||
|
platform: "macos",
|
||||||
|
outcome: "failure",
|
||||||
|
metadata: { release_channel: "stable" },
|
||||||
|
}),
|
||||||
|
testEnv({
|
||||||
|
diagnosticEvents,
|
||||||
|
kvEntries: [[authSessionCacheKvKey("local", tokenHash), sessionDocument(DEVICE_ID)]],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.status, 400);
|
||||||
|
assert.deepEqual(await response.json(), { error: "telemetry_unknown_field" });
|
||||||
|
assert.equal(diagnosticEvents.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects unsupported diagnostic event types", async () => {
|
||||||
|
const tokenHash = await authTokenHash(ACCESS_TOKEN);
|
||||||
|
const diagnosticEvents: ElyAnalyticsDataPoint[] = [];
|
||||||
|
const response = await handleRequest(
|
||||||
|
telemetryRequest({
|
||||||
|
version: 1,
|
||||||
|
event_type: "page_view",
|
||||||
|
occurred_at: 1_780_000_000,
|
||||||
|
app_version: "0.1.0",
|
||||||
|
platform: "macos",
|
||||||
|
}),
|
||||||
|
testEnv({
|
||||||
|
diagnosticEvents,
|
||||||
|
kvEntries: [[authSessionCacheKvKey("local", tokenHash), sessionDocument(DEVICE_ID)]],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.status, 400);
|
||||||
|
assert.deepEqual(await response.json(), { error: "telemetry_event_type_invalid" });
|
||||||
|
assert.equal(diagnosticEvents.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects unauthenticated diagnostic events before recording telemetry", async () => {
|
||||||
|
const diagnosticEvents: ElyAnalyticsDataPoint[] = [];
|
||||||
|
const response = await handleRequest(
|
||||||
|
new Request("https://elydora.test/api/telemetry/events", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
event_type: "app_crash",
|
||||||
|
occurred_at: 1_780_000_000,
|
||||||
|
app_version: "0.1.0",
|
||||||
|
platform: "macos",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
testEnv({ diagnosticEvents }),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.status, 401);
|
||||||
|
assert.deepEqual(await response.json(), { error: "authorization_missing" });
|
||||||
|
assert.equal(diagnosticEvents.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects unsupported methods before recording telemetry", async () => {
|
||||||
|
const diagnosticEvents: ElyAnalyticsDataPoint[] = [];
|
||||||
|
const response = await handleRequest(
|
||||||
|
new Request("https://elydora.test/api/telemetry/events", { method: "GET" }),
|
||||||
|
testEnv({ diagnosticEvents }),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(response.status, 405);
|
||||||
|
assert.equal(response.headers.get("allow"), "POST");
|
||||||
|
assert.deepEqual(await response.json(), { error: "method_not_allowed" });
|
||||||
|
assert.equal(diagnosticEvents.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function telemetryRequest(body: Record<string, unknown>): Request {
|
||||||
|
return new Request("https://elydora.test/api/telemetry/events", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${ACCESS_TOKEN}`,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -25,6 +25,10 @@ ELY_AUTH_BASE_URL = "http://localhost:8787"
|
|||||||
binding = "ELY_API_AUDIT"
|
binding = "ELY_API_AUDIT"
|
||||||
dataset = "ely_api_audit"
|
dataset = "ely_api_audit"
|
||||||
|
|
||||||
|
[[analytics_engine_datasets]]
|
||||||
|
binding = "ELY_DIAGNOSTICS"
|
||||||
|
dataset = "ely_diagnostics"
|
||||||
|
|
||||||
[[ratelimits]]
|
[[ratelimits]]
|
||||||
name = "ELY_RATE_LIMITER"
|
name = "ELY_RATE_LIMITER"
|
||||||
namespace_id = "1001"
|
namespace_id = "1001"
|
||||||
|
|||||||
Reference in New Issue
Block a user