Files
insights/playwright/e2e/ingestion-capture.spec.ts
zeekay 40d120dbf7 refactor(api): say /v1/ in the Python callers
Every first-party Python caller and test now names the mount the API actually
answers on. Mechanical: URL literals only, and only where the /api/ was ours --
a preceding word character means it was a module path like insights/api/ or a
third-party host like slack.com/api/, and those are untouched.

Left saying /api/, each for a reason rather than for safety:
- products/warehouse_sources/.../sources/** is a catalogue of vendor endpoints
- services/llm-gateway deploys separately and is under a code freeze, so moving
  its two calls would break it during the skew
- the mcp_store and Slack/Teams redirect_uris are registered with third parties
  and must byte-match what was registered
- the Wizard and Raycast client-metadata paths are OAuth client_ids
- /api/surveys and /api/feature_flag/local_evaluation are SDK aliases the
  ingress routes to the Rust fleets, held by released SDK versions

Test URLconfs moved with the requests that call them, and the Datastore
user-selection fixtures now carry the leading slash that request.path has.
2026-08-08 18:38:44 -07:00

63 lines
3.0 KiB
TypeScript

import { randomString } from '../utils'
import { expect, test } from '../utils/workspace-test-base'
// Capture and flags are served by separate Rust services fronted by the Caddy dev proxy,
// not by the Django server Playwright's baseURL points at.
const PROXY_BASE_URL = process.env.E2E_PROXY_URL || 'http://localhost:8010'
test.describe('Event ingestion', () => {
test('an event sent to the real capture endpoint becomes queryable', async ({ request, playwrightSetup }) => {
test.setTimeout(120_000)
const workspace = await playwrightSetup.createWorkspace({ skip_onboarding: true, no_demo_data: true })
const eventName = randomString('e2e_ingestion_')
const authHeaders = { Authorization: `Bearer ${workspace.personal_api_key}` }
let apiToken = ''
await test.step('fetch the team api token', async () => {
const team = await request.get(`/v1/environments/${workspace.team_id}/`, { headers: authHeaders })
expect(team.ok()).toBe(true)
apiToken = (await team.json()).api_token
})
await test.step('send an event through the capture endpoint', async () => {
const captured = await request.post(`${PROXY_BASE_URL}/e/`, {
data: {
api_key: apiToken,
event: eventName,
distinct_id: randomString('e2e-user-'),
timestamp: new Date().toISOString(),
},
})
expect(captured.ok()).toBe(true)
})
await test.step('poll until the event has been ingested and is queryable', async () => {
await expect
.poll(
async () => {
const resp = await request.post(`/v1/environments/${workspace.team_id}/query/`, {
headers: authHeaders,
data: {
// Without this the endpoint serves a cached result for up to 5 minutes,
// so the first poll's zero would be replayed for the rest of the window.
refresh: 'force_blocking',
query: {
kind: 'InsightsQLQuery',
query: `SELECT count() FROM events WHERE event = '${eventName}'`,
},
},
})
// A 4xx other than rate limiting means the query itself is bad — waiting won't fix it.
if (resp.status() >= 400 && resp.status() < 500 && resp.status() !== 429) {
throw new Error(`query endpoint returned ${resp.status()}: ${await resp.text()}`)
}
const body = await resp.json()
return Number(body.results?.[0]?.[0] ?? 0)
},
{ timeout: 90_000, intervals: [2_000, 5_000] }
)
.toBeGreaterThan(0)
})
})
})