Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ const AlertForm: React.FunctionComponent<AlertFormProps> = ({ alert }) => {
validationErrors?.message,
),
})}
data-testid="alert-name-input"
placeholder="Name"
{...field}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { Separator } from "@/ui/separator";
import { Input } from "@/ui/input";
import SelectBox from "@/shared/SelectBox/SelectBox";
import { AlertFormType } from "./schema";
import { TRIGGER_CONFIG } from "./helpers";
import { TRIGGER_CONFIG, alertTriggerTestId } from "./helpers";
import { WINDOW_OPTIONS } from "./constants";
import { ALERT_EVENT_TYPE } from "@/types/alerts";
import { GuardrailTypes } from "@/types/guardrails";
Expand Down Expand Up @@ -361,7 +361,10 @@ const EventTriggers: React.FunctionComponent<EventTriggersProps> = ({
return (
<div key={field.id}>
<div className="flex items-stretch gap-4">
<div className="flex flex-auto flex-col gap-3">
<div
className="flex flex-auto flex-col gap-3"
data-testid={alertTriggerTestId(field.eventType)}
>
Comment thread
AndreiCautisanu marked this conversation as resolved.
<div className="flex gap-4">
<div className="flex flex-1 flex-col gap-1">
<Label className="comet-body-s-accented">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const WebhookSettings: React.FC<WebhookSettingsProps> = ({ form }) => {
className={cn({
"border-destructive": Boolean(validationErrors?.message),
})}
data-testid="alert-webhook-url-input"
placeholder="https://hooks.slack.com/services/..."
{...field}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ export const ALERT_TYPE_ICONS = {
[ALERT_TYPE.pagerduty]: PagerDutyIcon,
};

/**
* Test-id slug for an event type: the wire values carry `:` (`trace:cost`),
* which has no place in a selector. Shared with the E2E page object.
*/
export const alertTriggerTestId = (
eventType: ALERT_EVENT_TYPE | string,
): string => `alert-trigger-${String(eventType).replace(/:/g, "-")}`;

export const TRIGGER_CONFIG: Record<ALERT_EVENT_TYPE, TriggerConfig> = {
[ALERT_EVENT_TYPE.trace_errors]: {
title: "Trace errors threshold",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const AlertsActionsPanel: React.FunctionComponent<AlertsActionsPanelsProps> = ({
<Button
variant="outline"
size="icon-sm"
data-testid="alerts-bulk-delete-button"
onClick={() => {
setOpen(true);
resetKeyRef.current = resetKeyRef.current + 1;
Expand Down
25 changes: 16 additions & 9 deletions tests_end_to_end/coverage/taxonomy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -677,24 +677,31 @@ areas:
label: Alerts
nav_group: Production
routes: ["/projects/$projectId/alerts", "/alerts/new", "/alerts/$alertId"]
spec_dir: alerts
specs:
- alerts/alerts-crud.spec.ts
capabilities:
list-alerts: { covered: false }
create-alert: { covered: false }
edit-alert: { covered: false }
event-triggers: { covered: false }
webhook-destination: { covered: false }
enable-disable: { covered: false }
delete-alert: { covered: false }
list-alerts: { covered: true, tier: t1-smoke }
create-alert: { covered: true, tier: t1-smoke }
edit-alert: { covered: true, tier: t2-cuj }
event-triggers: { covered: true, tier: t2-cuj }
webhook-destination: { covered: true, tier: t1-smoke }
enable-disable: { covered: true, tier: t2-cuj }
delete-alert: { covered: true, tier: t2-cuj }
# "Test connection" / "Test trigger" POST the draft alert to the endpoint
# in the form. Asserting a *success* needs a receiver the suite controls;
# against example.com the mutation answers failure, so a passing test
# would only be pinning the error toast. Left uncovered until a receiver
# exists rather than covered by asserting the sad path.
test-alert: { covered: false }

# ===================== ASSISTANT / CONNECT =====================

visual:
alerts-empty: { covered: true, state: empty, spec: "empty-states.spec.ts E12" }
alerts-list: { covered: false, state: default }
alert-editor: { covered: false, state: default }

# ===================== ASSISTANT / CONNECT =====================

ollie:
label: Opik Connect
nav_group: Home
Expand Down
164 changes: 164 additions & 0 deletions tests_end_to_end/e2e/fixtures/alert.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { test as baseTest } from './dashboard-cleanup.fixture';
import { shouldLeaveArtifacts } from '../core/artifacts';
import { uuid7 } from '../core/backend/uuid7';

/** Wire values of `AlertTriggerWriteEventType`; the UI renders them under its own titles. */
export const ALERT_EVENT_TYPE = {
promptCreated: 'prompt:created',
promptCommitted: 'prompt:committed',
promptDeleted: 'prompt:deleted',
experimentFinished: 'experiment:finished',
traceCost: 'trace:cost',
traceLatency: 'trace:latency',
traceErrors: 'trace:errors',
} as const;

export type AlertEventType = (typeof ALERT_EVENT_TYPE)[keyof typeof ALERT_EVENT_TYPE];
Comment thread
AndreiCautisanu marked this conversation as resolved.

/** Trigger titles the alerts list and editor show for each event type. */
export const ALERT_EVENT_TITLE: Record<AlertEventType, string> = {
[ALERT_EVENT_TYPE.promptCreated]: 'New prompt added',
[ALERT_EVENT_TYPE.promptCommitted]: 'New prompt version created',
[ALERT_EVENT_TYPE.promptDeleted]: 'Prompt deleted',
[ALERT_EVENT_TYPE.experimentFinished]: 'Experiment finished',
[ALERT_EVENT_TYPE.traceCost]: 'Cost threshold',
[ALERT_EVENT_TYPE.traceLatency]: 'Latency threshold',
[ALERT_EVENT_TYPE.traceErrors]: 'Trace errors threshold',
};

export interface AlertSeed {
/** Appended to the test namespace, so one test can seed several alerts. */
suffix: string;
enabled?: boolean;
eventTypes?: AlertEventType[];
}

export interface AlertRef {
id: string;
name: string;
webhookUrl: string;
enabled: boolean;
eventTypes: AlertEventType[];
}

export interface AlertFixtures {
/**
* One alert under the `project` fixture: enabled, General destination, a
* single `prompt:created` trigger. That trigger is what makes it render a
* full list row — a trigger-less alert shows "-" under Events.
*/
alert: AlertRef;

/**
* Seeds extra alerts, for tests needing more than one row. A callback
* because the count and trigger mix differ per test; each is torn down
* alongside the `alert` fixture's own.
*/
seedAlerts: (seeds: AlertSeed[]) => Promise<AlertRef[]>;

/**
* Cleans up alerts a test creates through the UI, which have no id until the
* form submits and the row renders.
*
* Requesting the fixture is the whole API — alerts are found at teardown by
* the test's own namespace prefix rather than registered by the test. An
* id-registration call would be skipped by a failure between the create and
* the registration, leaking exactly the alert whose run went wrong.
*/
uiAlertCleanup: void;
}

/**
* Seeding and teardown for project-scoped alerts.
*
* Teardown is mandatory: alerts do not cascade with their project. `alerts`
* holds `project_id` as a plain indexed column with no FK, and
* `ProjectService.delete` only touches `ProjectDAO` — so an alert outlives the
* project that scoped it and surfaces in the next spec's list. Same hazard
* `automationRulesCleanup` documents for rules.
*
* Ids are minted client-side because `POST /v1/private/alerts` answers 201
* with no body; without them teardown would need a workspace-wide paginated
* read to find each alert by name.
*
* Best-effort: a failed delete warns rather than throws, so cleanup cannot
* mask the assertion failure that explains the run.
*/
export const test = baseTest.extend<AlertFixtures>({
seedAlerts: async ({ sdkClient, project, backendClient, testNamespace }, use, testInfo) => {
const created: AlertRef[] = [];

await use(async (seeds) => {
const refs: AlertRef[] = [];
for (const seed of seeds) {
const ref: AlertRef = {
id: uuid7(),
name: `${testNamespace}-alert-${seed.suffix}`,
webhookUrl: `https://example.com/e2e-webhook-${seed.suffix}`,
enabled: seed.enabled ?? true,
eventTypes: seed.eventTypes ?? [ALERT_EVENT_TYPE.promptCreated],
};
// The Python bridge has no alert routes; the TS SDK is the public surface.
await sdkClient.typescript.api.alerts.createAlert({
id: ref.id,
name: ref.name,
enabled: ref.enabled,
alertType: 'general',
projectId: project.id,
webhook: { url: ref.webhookUrl },
triggers: ref.eventTypes.map((eventType) => ({ eventType })),
});
created.push(ref);
refs.push(ref);
}
return refs;
});

if (created.length === 0) return;

await testInfo.attach('opik.alerts', {
body: JSON.stringify(created, null, 2),
contentType: 'application/json',
});

if (shouldLeaveArtifacts(testInfo)) {
console.warn(
`[alert fixture] leaving ${created.length} alert(s) under ${project.name} for debugging`,
);
return;
}

try {
await backendClient.deleteAlertsBatch(created.map((a) => a.id));
} catch (err) {
console.warn('[alert fixture] batch delete warning:', err);
}
},

alert: async ({ seedAlerts }, use) => {
const [seeded] = await seedAlerts([{ suffix: 'seeded' }]);
await use(seeded);
},

uiAlertCleanup: [
async ({ backendClient, testNamespace }, use, testInfo) => {
await use();

if (shouldLeaveArtifacts(testInfo)) return;

// Deletes by prefix, so this also sweeps the fixture-seeded alerts —
// harmless, since `deleteAlertsBatch` is idempotent for ids already gone.
try {
const leftover = await backendClient.listAlertsWithPrefix(`${testNamespace}-alert-`);
await backendClient.deleteAlertsBatch(leftover.map((a) => a.id));
Comment thread
AndreiCautisanu marked this conversation as resolved.
Outdated
Comment thread
AndreiCautisanu marked this conversation as resolved.
Outdated
} catch (err) {
console.warn('[alert fixture] UI-created alert cleanup warning:', err);
}
},
// Opted into by name, so the extra workspace-wide read stays off every
// test that only seeds through `seedAlerts`.
{ auto: false },
],
});

export { expect } from './dashboard-cleanup.fixture';
9 changes: 8 additions & 1 deletion tests_end_to_end/e2e/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { test, expect } from './dashboard-cleanup.fixture';
export { test, expect } from './alert.fixture';
export type {
OauthProviderSeed,
ProviderKeysFixture,
Expand Down Expand Up @@ -105,4 +105,11 @@ export type {
TraceAttachmentsFixtures,
} from './trace-attachments.fixture';
export type { DashboardCleanupFixtures } from './dashboard-cleanup.fixture';
export type {
AlertSeed,
AlertRef,
AlertEventType,
AlertFixtures,
} from './alert.fixture';
export { ALERT_EVENT_TYPE, ALERT_EVENT_TITLE } from './alert.fixture';
export type { ProjectRef } from '../core/backend';
Loading