ConusAI vendor v0.1.0-dev.20261013+85463d22
This commit is contained in:
parent
c3c6526952
commit
0da8a2a81b
+28
-3
@@ -6,9 +6,9 @@ import { createServer } from 'node:http'
|
||||
import { gunzipSync } from 'node:zlib'
|
||||
|
||||
const PORT = 4010
|
||||
const state = { audit: [], usage: [], blobs: [] }
|
||||
const state = { audit: [], usage: [], blobs: [], otel: { logs: [], traces: [] } }
|
||||
const catalog = {
|
||||
redis: { display_name: 'Redis', description: 'Counts uploads per visitor each minute.' },
|
||||
kv: { display_name: 'KV Store', description: 'Counts uploads per visitor each minute.' },
|
||||
nats: { display_name: 'NATS', description: 'Announces new notes live to subscribers.' },
|
||||
qdrant: { display_name: 'Qdrant', description: 'Finds similar notes by vector search.' },
|
||||
git: { display_name: 'Git & Packages', description: 'Installs packages from the platform registry.' },
|
||||
@@ -68,6 +68,16 @@ const server = createServer(async (req, res) => {
|
||||
return send(res, 200, { items, next_cursor: null, has_more: false })
|
||||
}
|
||||
|
||||
// @conusai-sdk/observe-node's BatchExporter (showcase D23): POSTs OTLP/HTTP JSON directly to
|
||||
// these `{CONUSAI_API_URL}/otel/v1/{signal}` routes, bearer-authenticated with CONUSAI_API_TOKEN
|
||||
// -- not the OTEL_EXPORTER_OTLP_* env vars, which the SDK never reads itself.
|
||||
if (method === 'POST' && (path === '/otel/v1/logs' || path === '/otel/v1/traces')) {
|
||||
const body = JSON.parse((await readBody(req)).toString('utf8'))
|
||||
const bucket = path.endsWith('/logs') ? state.otel.logs : state.otel.traces
|
||||
bucket.push({ authorization: req.headers['authorization'] ?? null, body })
|
||||
return send(res, 200, {})
|
||||
}
|
||||
|
||||
if (method === 'POST' && path === '/_conusai/usage/events') {
|
||||
const body = JSON.parse((await readBody(req)).toString('utf8'))
|
||||
state.usage.push(...body.events)
|
||||
@@ -83,9 +93,24 @@ const server = createServer(async (req, res) => {
|
||||
contentType: req.headers['content-type'] ?? 'application/octet-stream',
|
||||
size: bytes.length,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
bytes,
|
||||
}
|
||||
state.blobs.push(info)
|
||||
return send(res, 200, info)
|
||||
return send(res, 200, {
|
||||
url: info.url,
|
||||
pathname: info.pathname,
|
||||
contentType: info.contentType,
|
||||
size: info.size,
|
||||
uploadedAt: info.uploadedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /blob/{pathname}, and the real API's /blob/{project_id}/{pathname} shape.
|
||||
if (method === 'GET' && path.startsWith('/blob/')) {
|
||||
const info = state.blobs.find((blob) => path === `/blob/${blob.pathname}` || path.endsWith(`/${blob.pathname}`))
|
||||
if (!info) return send(res, 404, { title: 'not found', path })
|
||||
res.writeHead(200, { 'content-type': info.contentType })
|
||||
return res.end(info.bytes)
|
||||
}
|
||||
|
||||
return send(res, 404, { title: 'not found', path })
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
// Showcase D23: register() in instrumentation.ts starts the OTLP exporters when the platform
|
||||
// injects OTEL_EXPORTER_OTLP_ENDPOINT, and every server action logs through shared/api/observe's
|
||||
// logger(). This proves the whole chain against a real `next start` production server (not dev,
|
||||
// not a mock of register()): a log a server action emits reaches the fake platform's
|
||||
// /otel/v1/logs route within the exporter's 5 s batch interval.
|
||||
|
||||
const platform = 'http://127.0.0.1:4010'
|
||||
|
||||
interface LogRecordJson {
|
||||
body?: { stringValue?: string }
|
||||
attributes?: Array<{ key: string; value: { stringValue?: string } }>
|
||||
}
|
||||
|
||||
type OtelState = { otel: { logs: Array<{ authorization: string | null; body: { resourceLogs: Array<{ scopeLogs: Array<{ logRecords: LogRecordJson[] }> }> } }> } }
|
||||
|
||||
async function otelLogRecords(): Promise<LogRecordJson[]> {
|
||||
const response = await fetch(`${platform}/__e2e/state`)
|
||||
const state = (await response.json()) as OtelState
|
||||
return state.otel.logs.flatMap((post) => post.body.resourceLogs.flatMap((r) => r.scopeLogs.flatMap((s) => s.logRecords)))
|
||||
}
|
||||
|
||||
function attr(record: LogRecordJson, key: string): string | undefined {
|
||||
return record.attributes?.find((kv) => kv.key === key)?.value.stringValue
|
||||
}
|
||||
|
||||
async function createNote(page: Page, title: string): Promise<void> {
|
||||
await page.goto('/')
|
||||
await page.getByLabel('Title').fill(title)
|
||||
await page.getByLabel('Body').fill('observability e2e')
|
||||
await page.getByRole('button', { name: 'Create note' }).click()
|
||||
await expect(page.getByRole('heading', { name: title })).toBeVisible()
|
||||
}
|
||||
|
||||
test('a note.created server action log reaches the OTLP receiver within the batch interval', async ({ page }) => {
|
||||
const title = `otlp ${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
|
||||
await createNote(page, title)
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => (await otelLogRecords()).some((r) => r.body?.stringValue === 'note.created' && attr(r, 'outcome') === 'ok'),
|
||||
// BatchExporter's default flush interval is 5 s (exporter.ts); this allows one full
|
||||
// interval plus slack for the request round trip and CI scheduling jitter.
|
||||
{ timeout: 9_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
test('the OTLP request carries the deployment token, not the public client', async ({ page }) => {
|
||||
const title = `otlp-auth ${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
|
||||
await createNote(page, title)
|
||||
|
||||
await expect
|
||||
.poll(async () => (await otelLogRecords()).some((r) => r.body?.stringValue === 'note.created'), { timeout: 9_000 })
|
||||
.toBe(true)
|
||||
|
||||
const response = await fetch(`${platform}/__e2e/state`)
|
||||
const state = (await response.json()) as OtelState
|
||||
expect(state.otel.logs.every((post) => post.authorization === 'Bearer e2e-deployment-token')).toBe(true)
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
// Proves, against a real production build (next build + next start, plan D18), that a
|
||||
// visit actually uploads a session replay segment — not just that the SDK is configured
|
||||
// to try. instrumentation-client.ts's /api/_conusai/* calls are same-origin and answered
|
||||
// by the platform's edge on a real deployment (docs/how-to/use-platform-analytics-in-an-
|
||||
// app.md §"Why this works"); this app has no edge in front of it here, so the test stands
|
||||
// in for it with the minimum the SDK needs: a session id from /ids, and a place to post
|
||||
// the envelope. This caught the stage regression (2026-10-11, showcase plan D24) where
|
||||
// mode: 'always' alone produced zero /api/_conusai/envelope requests because replay is a
|
||||
// 'consented' item and the default consent mode never sends those.
|
||||
|
||||
const SESSION_ID = '11111111-1111-1111-1111-111111111111'
|
||||
|
||||
test('a visit uploads a session replay segment within 15 s on a production build', async ({ page }) => {
|
||||
let envelopePosts = 0
|
||||
|
||||
// Analytics, vitals and errors also call the same-origin /api/_conusai/* paths; answer
|
||||
// them all generically so the run is quiet and deterministic, except the two the
|
||||
// recorder actually needs.
|
||||
await page.route('**/api/_conusai/**', (route) => {
|
||||
const url = route.request().url()
|
||||
if (url.includes('/api/_conusai/ids')) {
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ v: 1, session_id: SESSION_ID }) })
|
||||
}
|
||||
if (url.includes('/api/_conusai/envelope')) {
|
||||
envelopePosts += 1
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: '{}' })
|
||||
}
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: '{}' })
|
||||
})
|
||||
|
||||
await page.goto('/')
|
||||
await page.getByLabel('Title').click()
|
||||
await page.mouse.wheel(0, 200)
|
||||
await page.goto('/audit')
|
||||
await page.goto('/')
|
||||
|
||||
await expect.poll(() => envelopePosts, { timeout: 15_000, message: 'no POST to /api/_conusai/envelope (replay segment)' }).toBeGreaterThan(0)
|
||||
})
|
||||
@@ -61,13 +61,21 @@ test('an attachment moves the usage meter and is metered on the platform', async
|
||||
await expect
|
||||
.poll(async () => (await platformState()).usage.length, { timeout: 15_000 })
|
||||
.toBeGreaterThan(usageBefore)
|
||||
|
||||
// The attachment downloads through the app: the platform's blob URL needs the
|
||||
// deployment token, which the browser does not have.
|
||||
const href = await note.getByRole('link', { name: 'notes.txt' }).getAttribute('href')
|
||||
expect(href).toMatch(/^\/files\/[0-9a-f-]+$/)
|
||||
const download = await page.request.get(href ?? '')
|
||||
expect(download.status()).toBe(200)
|
||||
expect(await download.text()).toBe('sixteen bytes!!!')
|
||||
})
|
||||
|
||||
test('each optional module without its variables shows its enable card', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
|
||||
await expect(page.getByText(/to enable this\.$/)).toHaveCount(5)
|
||||
await expect(page.getByText('Link Redis to enable this.')).toBeVisible()
|
||||
await expect(page.getByText('Link KV Store to enable this.')).toBeVisible()
|
||||
await expect(page.getByText(/uploads left this minute/)).toHaveCount(0)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user