chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
import { describe, test, expect, beforeAll, afterAll } from "vitest";
|
||||
import type { PrismaClient } from "@trigger.dev/database";
|
||||
import type * as DeliverErrorGroupAlertModule from "../app/v3/services/alerts/deliverErrorGroupAlert.server.js";
|
||||
import type * as SecretStoreModule from "../app/services/secrets/secretStore.server.js";
|
||||
|
||||
let DeliverErrorGroupAlertService: typeof DeliverErrorGroupAlertModule.DeliverErrorGroupAlertService;
|
||||
let prisma: PrismaClient;
|
||||
let getSecretStore: typeof SecretStoreModule.getSecretStore;
|
||||
|
||||
type ErrorAlertPayload = {
|
||||
channelId: string;
|
||||
projectId: string;
|
||||
classification: "new_issue" | "regression" | "unignored";
|
||||
error: {
|
||||
fingerprint: string;
|
||||
environmentId: string;
|
||||
environmentSlug: string;
|
||||
environmentName: string;
|
||||
taskIdentifier: string;
|
||||
errorType: string;
|
||||
errorMessage: string;
|
||||
sampleStackTrace: string;
|
||||
firstSeen: string;
|
||||
lastSeen: string;
|
||||
occurrenceCount: number;
|
||||
};
|
||||
};
|
||||
|
||||
let testChannelId: string;
|
||||
let testProjectId: string;
|
||||
let testOrganizationId: string;
|
||||
let testSecretKey: string;
|
||||
let testSecretReferenceId: string;
|
||||
|
||||
// Helper to create mock error payloads
|
||||
function createMockErrorPayload(
|
||||
overrides: Partial<Omit<ErrorAlertPayload, "error">> & {
|
||||
error?: Partial<ErrorAlertPayload["error"]>;
|
||||
} = {}
|
||||
): ErrorAlertPayload {
|
||||
const { error: errorOverrides, ...payloadOverrides } = overrides;
|
||||
|
||||
const defaultError: ErrorAlertPayload["error"] = {
|
||||
fingerprint: "fp_test_" + Date.now(),
|
||||
environmentId: "env_test_dev",
|
||||
environmentSlug: "dev",
|
||||
environmentName: "Development",
|
||||
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)
|
||||
at TaskExecutor.execute (node_modules/@trigger.dev/core/dist/index.js:234:18)`,
|
||||
firstSeen: Date.now().toString(),
|
||||
lastSeen: Date.now().toString(),
|
||||
occurrenceCount: 42,
|
||||
...errorOverrides,
|
||||
};
|
||||
|
||||
return {
|
||||
channelId: testChannelId,
|
||||
projectId: testProjectId,
|
||||
classification: "new_issue",
|
||||
...payloadOverrides,
|
||||
error: defaultError,
|
||||
};
|
||||
}
|
||||
|
||||
// Skip tests if Slack credentials not configured
|
||||
const hasSlackCredentials =
|
||||
!!process.env.TEST_SLACK_CHANNEL_ID && !!process.env.TEST_SLACK_BOT_TOKEN;
|
||||
|
||||
describe.skipIf(!hasSlackCredentials)("Slack Error Alert Visual Tests", () => {
|
||||
beforeAll(async () => {
|
||||
const dbModule = await import("../app/db.server.js");
|
||||
prisma = dbModule.prisma;
|
||||
const secretModule = await import("../app/services/secrets/secretStore.server.js");
|
||||
getSecretStore = secretModule.getSecretStore;
|
||||
const alertModule = await import("../app/v3/services/alerts/deliverErrorGroupAlert.server.js");
|
||||
DeliverErrorGroupAlertService = alertModule.DeliverErrorGroupAlertService;
|
||||
|
||||
const organization = await prisma.organization.create({
|
||||
data: {
|
||||
title: "Slack Test Org",
|
||||
slug: "slack-test-org-" + Date.now(),
|
||||
},
|
||||
});
|
||||
testOrganizationId = organization.id;
|
||||
|
||||
// Create test project
|
||||
const project = await prisma.project.create({
|
||||
data: {
|
||||
name: "Slack Test Project",
|
||||
slug: "slack-test-project-" + Date.now(),
|
||||
externalRef: "proj_slack_test_" + Date.now(),
|
||||
organizationId: organization.id,
|
||||
},
|
||||
});
|
||||
testProjectId = project.id;
|
||||
|
||||
const secretStore = getSecretStore("DATABASE");
|
||||
testSecretKey = `slack-test-token-${Date.now()}`;
|
||||
|
||||
await secretStore.setSecret(testSecretKey, {
|
||||
botAccessToken: process.env.TEST_SLACK_BOT_TOKEN!,
|
||||
});
|
||||
|
||||
const secretReference = await prisma.secretReference.create({
|
||||
data: {
|
||||
key: testSecretKey,
|
||||
provider: "DATABASE",
|
||||
},
|
||||
});
|
||||
testSecretReferenceId = secretReference.id;
|
||||
|
||||
// Create Slack organization integration
|
||||
const integration = await prisma.organizationIntegration.create({
|
||||
data: {
|
||||
friendlyId: "integration_test_" + Date.now(),
|
||||
organizationId: organization.id,
|
||||
service: "SLACK",
|
||||
integrationData: {},
|
||||
tokenReferenceId: secretReference.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Create alert channel
|
||||
const channel = await prisma.projectAlertChannel.create({
|
||||
data: {
|
||||
friendlyId: "channel_test_" + Date.now(),
|
||||
name: "Test Slack Channel",
|
||||
type: "SLACK",
|
||||
enabled: true,
|
||||
projectId: project.id,
|
||||
integrationId: integration.id,
|
||||
properties: {
|
||||
channelId: process.env.TEST_SLACK_CHANNEL_ID!,
|
||||
channelName: "test-slack-alerts",
|
||||
integrationId: integration.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
testChannelId = channel.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (testChannelId) {
|
||||
await prisma.projectAlertChannel.deleteMany({
|
||||
where: { id: testChannelId },
|
||||
});
|
||||
}
|
||||
if (testOrganizationId) {
|
||||
await prisma.organizationIntegration.deleteMany({
|
||||
where: { organizationId: testOrganizationId },
|
||||
});
|
||||
}
|
||||
if (testSecretReferenceId) {
|
||||
await prisma.secretReference.deleteMany({
|
||||
where: { id: testSecretReferenceId },
|
||||
});
|
||||
}
|
||||
if (testSecretKey) {
|
||||
const secretStore = getSecretStore("DATABASE");
|
||||
await secretStore.deleteSecret(testSecretKey);
|
||||
}
|
||||
if (testProjectId) {
|
||||
await prisma.project.deleteMany({
|
||||
where: { id: testProjectId },
|
||||
});
|
||||
}
|
||||
if (testOrganizationId) {
|
||||
await prisma.organization.deleteMany({
|
||||
where: { id: testOrganizationId },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("new_issue classification", async () => {
|
||||
const payload = createMockErrorPayload({
|
||||
classification: "new_issue",
|
||||
error: {
|
||||
taskIdentifier: "process-order",
|
||||
errorMessage: "Failed to process order due to invalid payment method",
|
||||
errorType: "PaymentError",
|
||||
},
|
||||
});
|
||||
|
||||
const service = new DeliverErrorGroupAlertService();
|
||||
await service.call(payload);
|
||||
|
||||
// Message sent - check Slack channel visually
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("regression classification", async () => {
|
||||
const payload = createMockErrorPayload({
|
||||
classification: "regression",
|
||||
error: {
|
||||
taskIdentifier: "send-email",
|
||||
errorMessage: "SMTP connection timeout after 30 seconds",
|
||||
errorType: "TimeoutError",
|
||||
occurrenceCount: 156,
|
||||
},
|
||||
});
|
||||
|
||||
const service = new DeliverErrorGroupAlertService();
|
||||
await service.call(payload);
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("unignored (resurfaced) classification", async () => {
|
||||
const payload = createMockErrorPayload({
|
||||
classification: "unignored",
|
||||
error: {
|
||||
taskIdentifier: "sync-database",
|
||||
errorMessage: "Connection pool exhausted",
|
||||
errorType: "DatabaseError",
|
||||
occurrenceCount: 99,
|
||||
},
|
||||
});
|
||||
|
||||
const service = new DeliverErrorGroupAlertService();
|
||||
await service.call(payload);
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("short error message", async () => {
|
||||
const payload = createMockErrorPayload({
|
||||
error: {
|
||||
errorMessage: "Not found",
|
||||
errorType: "NotFoundError",
|
||||
sampleStackTrace: "NotFoundError: Not found\n at findUser (src/db.ts:10:5)",
|
||||
},
|
||||
});
|
||||
|
||||
const service = new DeliverErrorGroupAlertService();
|
||||
await service.call(payload);
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("long stack trace", async () => {
|
||||
const longStackTrace = `ReferenceError: processData is not defined
|
||||
at handler (src/tasks/data-processor.ts:125:15)
|
||||
at async TaskRunner.execute (node_modules/@trigger.dev/sdk/dist/runner.js:89:12)
|
||||
at async WorkerThread.processTask (node_modules/@trigger.dev/sdk/dist/worker.js:234:18)
|
||||
at async WorkerPool.run (src/lib/worker-pool.ts:56:10)
|
||||
at async TaskQueue.dequeue (src/lib/queue.ts:142:8)
|
||||
at async Orchestrator.processNextTask (src/orchestrator.ts:98:5)
|
||||
at async Orchestrator.start (src/orchestrator.ts:45:7)
|
||||
at async main (src/index.ts:12:3)
|
||||
at Object.<anonymous> (src/index.ts:20:1)
|
||||
at Module._compile (node:internal/modules/cjs/loader:1376:14)
|
||||
at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)
|
||||
at Module.load (node:internal/modules/cjs/loader:1207:32)
|
||||
at Module._load (node:internal/modules/cjs/loader:1023:12)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)
|
||||
at node:internal/main/run_main_module:28:49`;
|
||||
|
||||
const payload = createMockErrorPayload({
|
||||
error: {
|
||||
errorType: "ReferenceError",
|
||||
errorMessage: "processData is not defined",
|
||||
sampleStackTrace: longStackTrace,
|
||||
taskIdentifier: "data-processor",
|
||||
},
|
||||
});
|
||||
|
||||
const service = new DeliverErrorGroupAlertService();
|
||||
await service.call(payload);
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("very long error message (triggers truncation)", async () => {
|
||||
// Create a message that's over 3000 characters
|
||||
const longMessage = "x".repeat(3500);
|
||||
const longStackTrace = `Error: ${longMessage}
|
||||
at verylongfunctionname (src/tasks/long-task.ts:1:1)
|
||||
${" at stackframe (file.ts:1:1)\n".repeat(100)}`;
|
||||
|
||||
const payload = createMockErrorPayload({
|
||||
error: {
|
||||
errorMessage: longMessage,
|
||||
sampleStackTrace: longStackTrace,
|
||||
taskIdentifier: "long-error-task",
|
||||
errorType: "Error",
|
||||
},
|
||||
});
|
||||
|
||||
const service = new DeliverErrorGroupAlertService();
|
||||
await service.call(payload);
|
||||
|
||||
// Should see truncation message in Slack
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("special characters in error", async () => {
|
||||
const payload = createMockErrorPayload({
|
||||
error: {
|
||||
errorMessage: "Unexpected token `<` in JSON at position 0",
|
||||
errorType: "SyntaxError",
|
||||
sampleStackTrace: `SyntaxError: Unexpected token \`<\` in JSON at position 0
|
||||
at JSON.parse (<anonymous>)
|
||||
at parseResponse (src/api/client.ts:42:15)
|
||||
at fetch("https://api.example.com/data?query=test&limit=10")`,
|
||||
taskIdentifier: "api-fetch-task",
|
||||
},
|
||||
});
|
||||
|
||||
const service = new DeliverErrorGroupAlertService();
|
||||
await service.call(payload);
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("unicode and emoji in error", async () => {
|
||||
const payload = createMockErrorPayload({
|
||||
error: {
|
||||
errorMessage: "Failed to process emoji 🔥 in message: Hello 世界",
|
||||
errorType: "EncodingError",
|
||||
sampleStackTrace: `EncodingError: Failed to process emoji 🔥 in message: Hello 世界
|
||||
at encodeMessage (src/utils/encoding.ts:15:10)
|
||||
at sendMessage (src/tasks/messaging.ts:42:8)`,
|
||||
taskIdentifier: "messaging-task",
|
||||
},
|
||||
});
|
||||
|
||||
const service = new DeliverErrorGroupAlertService();
|
||||
await service.call(payload);
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("different error types - TypeError", async () => {
|
||||
const payload = createMockErrorPayload({
|
||||
error: {
|
||||
errorType: "TypeError",
|
||||
errorMessage: "Cannot call method 'map' on undefined",
|
||||
sampleStackTrace: `TypeError: Cannot call method 'map' on undefined
|
||||
at transformData (src/transformers/data.ts:18:25)`,
|
||||
},
|
||||
});
|
||||
|
||||
const service = new DeliverErrorGroupAlertService();
|
||||
await service.call(payload);
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("different error types - ReferenceError", async () => {
|
||||
const payload = createMockErrorPayload({
|
||||
error: {
|
||||
errorType: "ReferenceError",
|
||||
errorMessage: "userConfig is not defined",
|
||||
sampleStackTrace: `ReferenceError: userConfig is not defined
|
||||
at initializeApp (src/app.ts:32:10)`,
|
||||
},
|
||||
});
|
||||
|
||||
const service = new DeliverErrorGroupAlertService();
|
||||
await service.call(payload);
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("different error types - Custom Error", async () => {
|
||||
const payload = createMockErrorPayload({
|
||||
error: {
|
||||
errorType: "InvalidConfigurationError",
|
||||
errorMessage: "API key is missing or invalid",
|
||||
sampleStackTrace: `InvalidConfigurationError: API key is missing or invalid
|
||||
at validateConfig (src/config/validator.ts:45:11)
|
||||
at loadConfig (src/config/loader.ts:23:5)`,
|
||||
taskIdentifier: "config-loader",
|
||||
},
|
||||
});
|
||||
|
||||
const service = new DeliverErrorGroupAlertService();
|
||||
await service.call(payload);
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
test("error with no stack trace", async () => {
|
||||
const payload = createMockErrorPayload({
|
||||
error: {
|
||||
errorMessage: "An unknown error occurred",
|
||||
errorType: "Error",
|
||||
sampleStackTrace: "",
|
||||
},
|
||||
});
|
||||
|
||||
const service = new DeliverErrorGroupAlertService();
|
||||
await service.call(payload);
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user