Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions .lore.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
### Gotcha

<!-- lore:019ed0b6-c2c6-7197-9cec-2026914ec51d -->
* **esbuild override cap in getsentry/spotlight — resolved at 0.28.1**: The pnpm override \`"esbuild": ">=0.25.0 <0.28.0"\` was a deliberate cap to avoid esbuild#4436 (erroring on destructuring for old targets). Trap: bumping to \`>=0.28.1\` looks risky because 0.28.x retained that behavior. Fix: the regression did NOT reappear in practice — website build succeeded without adding \`target: "es2020"\`. Override is now \`"esbuild": ">=0.28.1"\`, resolving Dependabot alerts #279 and #280.
* **esbuild override cap in getsentry/spotlight — resolved at 0.28.1**: The pnpm override \`"esbuild": ">=0.25.0 <0.28.0"\` was a deliberate cap to avoid esbuild#4436 (erroring on destructuring for old targets). Trap: bumping to \`>=0.28.1\` looks risky because 0.28.x retained that behavior. Fix: the regression did NOT reappear in practice — website build succeeded without adding \`target: "es2020"\`. Override is now \`"esbuild": ">=0.28.1"\`, resolving Dependabot alerts #279 and #280. These alerts were merged via PR #1323 on 2026-06-16 and will auto-close without further action.

<!-- lore:019e2b7e-99e1-7402-8c4f-9699919d7e69 -->
* **plist override breaks electron-builder osx-sign**: Forcing \`plist>=3.1.1\` via pnpm overrides bumps it to v5.x, which breaks \`@electron/osx-sign@1.0.5\` (used by \`electron-builder@24.13.3\`) due to incompatible CJS \`require()\` and new \`exports\` map. Fix: remove the \`plist\` override and instead override \`@xmldom/xmldom\` directly to \`>=0.8.13\` (first patched 0.8.x version). This keeps \`plist@3.1.0\` for osx-sign compatibility while eliminating the \`@xmldom/xmldom\` vulnerability.

### Pattern

<!-- lore:019ed0b6-c2ce-7193-9376-5e711e5d5441 -->
* **Security dep-bump workflow in getsentry/spotlight**: Pattern for resolving Dependabot alerts: (1) fetch alerts via \`gh api /repos/{owner}/{repo}/dependabot/alerts\`; (2) plan fix in \`.opencode/plans/\`; (3) bump pnpm override in root \`package.json\`; (4) run \`pnpm install\`, verify lockfile, run full \`pnpm build\` + \`vitest run\`; (5) create branch \`security/deps-\<pkg>-\<version>\` off main, commit, push, open PR. Untracked \`.opencode/\` and \`packages/website/content.config.ts\` are intentionally excluded from security commits.
* **Security dep-bump workflow in getsentry/spotlight**: Pattern for resolving Dependabot alerts in getsentry/spotlight: (1) fetch alerts via \`gh api /repos/{owner}/{repo}/dependabot/alerts\`; (2) plan fix in \`.opencode/plans/\`; (3) bump pnpm overrides in root \`package.json\` AND bump direct deps in affected \`packages/\*/package.json\` where needed (e.g. astro direct dep); (4) run \`pnpm install\`, verify lockfile, run full \`pnpm build\` + \`vitest run\`; (5) create branch \`security/deps-\<descriptor>\` off main, commit, push, open PR. Vite major-version overrides must be bounded (e.g. \`<8\`) to prevent accidental major jumps. Untracked \`.opencode/\` and \`packages/website/content.config.ts\` are intentionally excluded from security commits. E2E UI test flakes are known — rerun before investigating.
86 changes: 41 additions & 45 deletions packages/spotlight/tests/e2e/ui/attachments.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { expect, test } from "./fixtures";
import { test, waitForAppReady } from "./fixtures";

test.describe("Attachments Display UI Tests", () => {
test("should display envelope with screenshot", async ({ page, sidecar, sendTestEnvelope }) => {
await page.goto(sidecar.baseURL);
await waitForAppReady(page);

// Send envelope with screenshot (binary)
await sendTestEnvelope("envelope_with_screenshot.bin");

Expand All @@ -14,102 +16,98 @@ test.describe("Attachments Display UI Tests", () => {
.click()
.catch(() => {});

// Wait and verify some content is displayed
const pageContent = page.locator("body");
const text = await pageContent.textContent();
expect(text).not.toBe("");
// The app should still be rendered after interacting with it
await waitForAppReady(page);
});

test("should handle Flutter replay binary", async ({ page, sidecar, sendTestEnvelope }) => {
await page.goto(sidecar.baseURL);
await waitForAppReady(page);

// Send Flutter replay binary
await sendTestEnvelope("envelope_flutter_replay.bin");

// Verify content is displayed
const pageContent = page.locator("body");
const text = await pageContent.textContent();
expect(text).not.toBe("");
// The UI should not crash on this payload
await waitForAppReady(page);
});

test("should handle browser JS profile", async ({ page, sidecar, sendTestEnvelope }) => {
await page.goto(sidecar.baseURL);
await waitForAppReady(page);

// Send browser JS profile binary
await sendTestEnvelope("enveplope_browser_js_profile.bin");

// Verify content is displayed
const pageContent = page.locator("body");
const text = await pageContent.textContent();
expect(text).not.toBe("");
// The UI should not crash on this payload
await waitForAppReady(page);
});

test("should handle generic binary envelope", async ({ page, sidecar, sendTestEnvelope }) => {
await page.goto(sidecar.baseURL);
await waitForAppReady(page);

// Send generic binary envelope
await sendTestEnvelope("envelope_binary.bin");

// Verify the UI doesn't crash and displays something
const pageContent = page.locator("body");
const text = await pageContent.textContent();
expect(text).not.toBe("");
// Verify the UI doesn't crash and is still rendered
await waitForAppReady(page);
});

test("should display attachments from fixture directory", async ({ page, sidecar }) => {
await page.goto(sidecar.baseURL);
// The test verifies that the UI can handle attachment data
// Wait for page to be ready
const pageContent = page.locator("body");
await pageContent.waitFor({ timeout: 5000 });

const text = await pageContent.textContent();
expect(text).not.toBe("");
// The test verifies that the UI renders without any telemetry present
await waitForAppReady(page);
});

test("should handle empty payload", async ({ page, sidecar, sendTestEnvelope }) => {
await page.goto(sidecar.baseURL);
await waitForAppReady(page);

// Send empty payload envelope
await sendTestEnvelope("envelope_empty_payload.txt");

// Should not crash
const pageContent = page.locator("body");
const text = await pageContent.textContent();
expect(text).not.toBe("");
await waitForAppReady(page);
});

test("should handle empty envelope", async ({ page, sidecar, sendTestEnvelope }) => {
await page.goto(sidecar.baseURL);
await waitForAppReady(page);

// Send empty envelope
await sendTestEnvelope("envelope_empty.txt");

// Should not crash
const pageContent = page.locator("body");
const text = await pageContent.textContent();
expect(text).not.toBe("");
await waitForAppReady(page);
});

test("should display image attachments if present", async ({ page, sidecar, sendTestEnvelope }) => {
await page.goto(sidecar.baseURL);
await waitForAppReady(page);

// Send envelope with screenshot
await sendTestEnvelope("envelope_with_screenshot.bin");

// Look for image elements
// Look for image elements (best-effort, the payload may not surface one)
const _hasImage = await page
.locator('img, [role="img"]')
.first()
.isVisible()
.catch(() => false);

// At minimum, page should have content
const pageContent = page.locator("body");
const text = await pageContent.textContent();
expect(text).not.toBe("");
// At minimum, the app should still be rendered
await waitForAppReady(page);
});

test("should provide download capability for attachments", async ({ page, sidecar, sendTestEnvelope }) => {
await page.goto(sidecar.baseURL);
await waitForAppReady(page);

// Send envelope with attachment
await sendTestEnvelope("envelope_with_screenshot.bin");

// Look for download buttons or links
// Look for download buttons or links (best-effort)
const _hasDownloadLink = await Promise.race([
page
.locator('a[download], button[aria-label*="download"], [title*="download"]')
Expand All @@ -120,32 +118,30 @@ test.describe("Attachments Display UI Tests", () => {
new Promise<boolean>(resolve => setTimeout(() => resolve(false), 2000)),
]);

// At minimum, page should render
const pageContent = page.locator("body");
await pageContent.waitFor({ timeout: 5000 });
expect(await pageContent.isVisible()).toBe(true);
// At minimum, the app should still be rendered
await waitForAppReady(page);
});

test("should handle various attachment content types", async ({ page, sidecar, sendTestEnvelope }) => {
await page.goto(sidecar.baseURL);
await waitForAppReady(page);

// Send multiple envelopes with different content
await sendTestEnvelope("envelope_javascript.txt");
await sendTestEnvelope("envelope_python.txt");

// Should handle various types without crashing
const pageContent = page.locator("body");
const text = await pageContent.textContent();
expect(text).not.toBe("");
await waitForAppReady(page);
});

test("should handle envelope with no length and EOF", async ({ page, sidecar, sendTestEnvelope }) => {
await page.goto(sidecar.baseURL);
await waitForAppReady(page);

// Send envelope with no length and EOF
await sendTestEnvelope("envelope_no_len_w_eof.txt");

// Should not crash
const pageContent = page.locator("body");
const text = await pageContent.textContent();
expect(text).not.toBe("");
await waitForAppReady(page);
});
});
15 changes: 14 additions & 1 deletion packages/spotlight/tests/e2e/ui/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fs from "node:fs/promises";
import http from "node:http";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { type Page, test as base } from "@playwright/test";
import { type Page, test as base, expect } from "@playwright/test";
import { findFreePort, getFixturePath, killProcess, spawnProcess } from "../shared/utils";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -120,6 +120,19 @@ async function sendEnvelopeToSidecar(
});
}

/**
* Wait for the Spotlight UI to finish its initial render.
*
* The navigation sidebar (`nav[aria-label="Navigation"]`) is rendered
* unconditionally as soon as the React app mounts, regardless of whether any
* telemetry has been received. Asserting on it with a web-first (auto-retrying)
* assertion is far more reliable than reading `body.textContent()` once, which
* races React's first paint and previously made these tests flaky.
*/
export async function waitForAppReady(page: Page, timeout = 15000): Promise<void> {
await expect(page.locator('nav[aria-label="Navigation"]')).toBeVisible({ timeout });
}

/**
* Wait for an event to appear in the UI
*/
Expand Down
Loading