249 lines
7.7 KiB
TypeScript
249 lines
7.7 KiB
TypeScript
import { describe, test, expect } from "vitest";
|
|
import { Webhook } from "@trigger.dev/core/v3/schemas";
|
|
import {
|
|
generateErrorGroupWebhookPayload,
|
|
type ErrorGroupAlertData,
|
|
} from "~/v3/services/alerts/errorGroupWebhook.server";
|
|
|
|
function createMockAlertData(overrides: Partial<ErrorGroupAlertData> = {}): ErrorGroupAlertData {
|
|
const now = Date.now();
|
|
const earlier = now - 3600000; // 1 hour ago
|
|
|
|
return {
|
|
classification: "new_issue",
|
|
error: {
|
|
fingerprint: "fp_test_12345",
|
|
environmentId: "env_abc123",
|
|
environmentName: "Production",
|
|
taskIdentifier: "process-payment",
|
|
errorType: "TypeError",
|
|
errorMessage: "Cannot read property 'id' of undefined",
|
|
sampleStackTrace: `TypeError: Cannot read property 'id' of undefined
|
|
at processPayment (src/tasks/payment.ts:42:15)
|
|
at Object.run (src/tasks/payment.ts:15:20)`,
|
|
firstSeen: String(earlier),
|
|
lastSeen: String(now),
|
|
occurrenceCount: 5,
|
|
},
|
|
organization: {
|
|
id: "org_xyz789",
|
|
slug: "acme-corp",
|
|
name: "Acme Corp",
|
|
},
|
|
project: {
|
|
id: "proj_123",
|
|
externalRef: "proj_abc",
|
|
slug: "my-project",
|
|
name: "My Project",
|
|
},
|
|
dashboardUrl:
|
|
"https://cloud.trigger.dev/orgs/acme-corp/projects/my-project/errors/fp_test_12345",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("generateErrorGroupWebhookPayload", () => {
|
|
test("generates a valid webhook payload", () => {
|
|
const alertData = createMockAlertData();
|
|
const payload = generateErrorGroupWebhookPayload(alertData);
|
|
|
|
expect(payload).toMatchObject({
|
|
type: "alert.error",
|
|
object: {
|
|
classification: "new_issue",
|
|
error: {
|
|
fingerprint: "fp_test_12345",
|
|
type: "TypeError",
|
|
message: "Cannot read property 'id' of undefined",
|
|
taskIdentifier: "process-payment",
|
|
occurrenceCount: 5,
|
|
},
|
|
environment: {
|
|
id: "env_abc123",
|
|
name: "Production",
|
|
},
|
|
organization: {
|
|
id: "org_xyz789",
|
|
slug: "acme-corp",
|
|
name: "Acme Corp",
|
|
},
|
|
project: {
|
|
id: "proj_123",
|
|
ref: "proj_abc",
|
|
slug: "my-project",
|
|
name: "My Project",
|
|
},
|
|
dashboardUrl:
|
|
"https://cloud.trigger.dev/orgs/acme-corp/projects/my-project/errors/fp_test_12345",
|
|
},
|
|
});
|
|
|
|
expect(payload.id).toBeDefined();
|
|
expect(payload.created).toBeInstanceOf(Date);
|
|
expect(payload.webhookVersion).toBe("2025-01-01");
|
|
});
|
|
|
|
test("payload is valid according to Webhook schema", () => {
|
|
const alertData = createMockAlertData();
|
|
const payload = generateErrorGroupWebhookPayload(alertData);
|
|
|
|
const parsed = Webhook.parse(payload);
|
|
expect(parsed.type).toBe("alert.error");
|
|
});
|
|
|
|
test("payload can be serialized and deserialized", () => {
|
|
const alertData = createMockAlertData();
|
|
const payload = generateErrorGroupWebhookPayload(alertData);
|
|
|
|
// Serialize to JSON (simulating sending over HTTP)
|
|
const serialized = JSON.stringify(payload);
|
|
const deserialized = JSON.parse(serialized);
|
|
|
|
// Verify it can still be parsed by the schema
|
|
const parsed = Webhook.parse(deserialized);
|
|
expect(parsed.type).toBe("alert.error");
|
|
|
|
if (parsed.type === "alert.error") {
|
|
expect(parsed.object.classification).toBe("new_issue");
|
|
expect(parsed.object.error.fingerprint).toBe("fp_test_12345");
|
|
}
|
|
});
|
|
|
|
test("handles new_issue classification", () => {
|
|
const alertData = createMockAlertData({ classification: "new_issue" });
|
|
const payload = generateErrorGroupWebhookPayload(alertData);
|
|
const parsed = Webhook.parse(payload);
|
|
|
|
if (parsed.type === "alert.error") {
|
|
expect(parsed.object.classification).toBe("new_issue");
|
|
}
|
|
});
|
|
|
|
test("handles regression classification", () => {
|
|
const alertData = createMockAlertData({ classification: "regression" });
|
|
const payload = generateErrorGroupWebhookPayload(alertData);
|
|
const parsed = Webhook.parse(payload);
|
|
|
|
if (parsed.type === "alert.error") {
|
|
expect(parsed.object.classification).toBe("regression");
|
|
}
|
|
});
|
|
|
|
test("handles unignored classification", () => {
|
|
const alertData = createMockAlertData({ classification: "unignored" });
|
|
const payload = generateErrorGroupWebhookPayload(alertData);
|
|
const parsed = Webhook.parse(payload);
|
|
|
|
if (parsed.type === "alert.error") {
|
|
expect(parsed.object.classification).toBe("unignored");
|
|
}
|
|
});
|
|
|
|
test("handles empty stack trace", () => {
|
|
const alertData = createMockAlertData({
|
|
error: {
|
|
...createMockAlertData().error,
|
|
sampleStackTrace: "",
|
|
},
|
|
});
|
|
const payload = generateErrorGroupWebhookPayload(alertData);
|
|
const parsed = Webhook.parse(payload);
|
|
|
|
if (parsed.type === "alert.error") {
|
|
expect(parsed.object.error.stackTrace).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
test("includes stack trace when present", () => {
|
|
const stackTrace = "Error at line 42";
|
|
const alertData = createMockAlertData({
|
|
error: {
|
|
...createMockAlertData().error,
|
|
sampleStackTrace: stackTrace,
|
|
},
|
|
});
|
|
const payload = generateErrorGroupWebhookPayload(alertData);
|
|
const parsed = Webhook.parse(payload);
|
|
|
|
if (parsed.type === "alert.error") {
|
|
expect(parsed.object.error.stackTrace).toBe(stackTrace);
|
|
}
|
|
});
|
|
|
|
test("preserves date fields correctly", () => {
|
|
const firstSeen = new Date("2024-01-01T00:00:00Z");
|
|
const lastSeen = new Date("2024-01-02T12:00:00Z");
|
|
|
|
const alertData = createMockAlertData({
|
|
error: {
|
|
...createMockAlertData().error,
|
|
firstSeen: String(firstSeen.getTime()),
|
|
lastSeen: String(lastSeen.getTime()),
|
|
},
|
|
});
|
|
|
|
const payload = generateErrorGroupWebhookPayload(alertData);
|
|
const parsed = Webhook.parse(payload);
|
|
|
|
if (parsed.type === "alert.error") {
|
|
expect(parsed.object.error.firstSeen).toEqual(firstSeen);
|
|
expect(parsed.object.error.lastSeen).toEqual(lastSeen);
|
|
}
|
|
});
|
|
|
|
test("handles special characters in error messages", () => {
|
|
const alertData = createMockAlertData({
|
|
error: {
|
|
...createMockAlertData().error,
|
|
errorMessage: "Unexpected token `<` in JSON at position 0",
|
|
sampleStackTrace: `SyntaxError: Unexpected token \`<\` in JSON
|
|
at JSON.parse (<anonymous>)
|
|
at fetch("https://api.example.com/data?query=test&limit=10")`,
|
|
},
|
|
});
|
|
|
|
const payload = generateErrorGroupWebhookPayload(alertData);
|
|
const serialized = JSON.stringify(payload);
|
|
const deserialized = JSON.parse(serialized);
|
|
const parsed = Webhook.parse(deserialized);
|
|
|
|
if (parsed.type === "alert.error") {
|
|
expect(parsed.object.error.message).toBe("Unexpected token `<` in JSON at position 0");
|
|
}
|
|
});
|
|
|
|
test("handles unicode and emoji in error messages", () => {
|
|
const alertData = createMockAlertData({
|
|
error: {
|
|
...createMockAlertData().error,
|
|
errorMessage: "Failed to process emoji 🔥 in message: Hello 世界",
|
|
},
|
|
});
|
|
|
|
const payload = generateErrorGroupWebhookPayload(alertData);
|
|
const serialized = JSON.stringify(payload);
|
|
const deserialized = JSON.parse(serialized);
|
|
const parsed = Webhook.parse(deserialized);
|
|
|
|
if (parsed.type === "alert.error") {
|
|
expect(parsed.object.error.message).toBe("Failed to process emoji 🔥 in message: Hello 世界");
|
|
}
|
|
});
|
|
|
|
test("handles large occurrence counts", () => {
|
|
const alertData = createMockAlertData({
|
|
error: {
|
|
...createMockAlertData().error,
|
|
occurrenceCount: 999999,
|
|
},
|
|
});
|
|
|
|
const payload = generateErrorGroupWebhookPayload(alertData);
|
|
const parsed = Webhook.parse(payload);
|
|
|
|
if (parsed.type === "alert.error") {
|
|
expect(parsed.object.error.occurrenceCount).toBe(999999);
|
|
}
|
|
});
|
|
});
|