@volter/twin-standard 1.0.30 → 1.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/archive-source.d.ts +13 -0
- package/dist/src/archive-source.js +176 -0
- package/dist/src/check-sources.d.ts +19 -2
- package/dist/src/check-sources.js +95 -37
- package/dist/src/cli.js +52 -9
- package/dist/src/clients/ai-gateway.d.ts +1 -0
- package/dist/src/clients/ai-gateway.js +3 -0
- package/dist/src/clients/anthropic-messages.d.ts +10 -0
- package/dist/src/clients/anthropic-messages.js +52 -0
- package/dist/src/clients/anthropic.d.ts +1 -0
- package/dist/src/clients/anthropic.js +3 -0
- package/dist/src/clients/aws.d.ts +2 -0
- package/dist/src/clients/aws.js +137 -0
- package/dist/src/clients/cerebras.d.ts +1 -0
- package/dist/src/clients/cerebras.js +3 -0
- package/dist/src/clients/clerk.d.ts +2 -0
- package/dist/src/clients/clerk.js +99 -0
- package/dist/src/clients/cloudflare.d.ts +2 -0
- package/dist/src/clients/cloudflare.js +86 -0
- package/dist/src/clients/elevenlabs.d.ts +2 -0
- package/dist/src/clients/elevenlabs.js +39 -0
- package/dist/src/clients/gemini.d.ts +2 -0
- package/dist/src/clients/gemini.js +92 -0
- package/dist/src/clients/github.d.ts +2 -0
- package/dist/src/clients/github.js +71 -0
- package/dist/src/clients/groq.d.ts +1 -0
- package/dist/src/clients/groq.js +3 -0
- package/dist/src/clients/harness.d.ts +36 -0
- package/dist/src/clients/harness.js +74 -0
- package/dist/src/clients/merge.d.ts +1 -0
- package/dist/src/clients/merge.js +8 -0
- package/dist/src/clients/openai-compatible.d.ts +9 -0
- package/dist/src/clients/openai-compatible.js +51 -0
- package/dist/src/clients/openai.d.ts +2 -0
- package/dist/src/clients/openai.js +65 -0
- package/dist/src/clients/openrouter.d.ts +1 -0
- package/dist/src/clients/openrouter.js +3 -0
- package/dist/src/clients/perplexity.d.ts +1 -0
- package/dist/src/clients/perplexity.js +3 -0
- package/dist/src/clients/planetscale.d.ts +2 -0
- package/dist/src/clients/planetscale.js +109 -0
- package/dist/src/clients/posthog.d.ts +2 -0
- package/dist/src/clients/posthog.js +145 -0
- package/dist/src/clients/run.d.ts +1 -0
- package/dist/src/clients/run.js +25 -0
- package/dist/src/clients/sentry.d.ts +2 -0
- package/dist/src/clients/sentry.js +167 -0
- package/dist/src/clients/slack.d.ts +2 -0
- package/dist/src/clients/slack.js +107 -0
- package/dist/src/clients/stripe.d.ts +2 -0
- package/dist/src/clients/stripe.js +161 -0
- package/dist/src/clients/tinybird.d.ts +2 -0
- package/dist/src/clients/tinybird.js +71 -0
- package/dist/src/clients/tunnel.d.ts +2 -0
- package/dist/src/clients/tunnel.js +99 -0
- package/dist/src/clients/upstash.d.ts +2 -0
- package/dist/src/clients/upstash.js +143 -0
- package/dist/src/clients/volteridentity.d.ts +2 -0
- package/dist/src/clients/volteridentity.js +128 -0
- package/dist/src/clients/xai.d.ts +2 -0
- package/dist/src/clients/xai.js +23 -0
- package/dist/src/conformance.d.ts +33 -0
- package/dist/src/conformance.js +520 -0
- package/dist/src/derive.js +113 -22
- package/dist/src/gate.js +22 -16
- package/dist/src/index.d.ts +5 -3
- package/dist/src/index.js +5 -3
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +7 -0
- package/dist/src/p3-rules.js +0 -2
- package/dist/src/protocol-3.d.ts +1 -0
- package/dist/src/protocol-3.js +76 -30
- package/dist/src/published.d.ts +6 -0
- package/dist/src/published.js +65 -11
- package/dist/src/spec-documents.js +2 -1
- package/dist/src/spec-ir-client.d.ts +5 -1
- package/dist/src/spec-ir-client.js +12 -2
- package/dist/src/spec-ir.d.ts +7 -2
- package/dist/src/spec-ir.js +42 -10
- package/dist/src/verdict.d.ts +14 -0
- package/dist/src/verdict.js +30 -0
- package/dist/src/walk/behavior-journey.d.ts +47 -0
- package/dist/src/walk/behavior-journey.js +1138 -0
- package/dist/src/walk/git-client.d.ts +26 -0
- package/dist/src/walk/git-client.js +139 -0
- package/dist/src/walk/journey-kit.d.ts +445 -0
- package/dist/src/walk/journey-kit.js +743 -0
- package/dist/src/walk/shape.d.ts +54 -0
- package/dist/src/walk/shape.js +210 -0
- package/dist/src/walk/sse-example.d.ts +2 -0
- package/dist/src/walk/sse-example.js +32 -0
- package/dist/src/walk/vendor-fetch.d.ts +20 -0
- package/dist/src/walk/vendor-fetch.js +92 -0
- package/dist/src/walk/walk-database.d.ts +12 -0
- package/dist/src/walk/walk-database.js +96 -0
- package/dist/src/walk/xml-view.d.ts +2 -0
- package/dist/src/walk/xml-view.js +55 -0
- package/package.json +39 -3
- package/src/archive-source.ts +141 -0
- package/src/check-sources.ts +83 -33
- package/src/cli.ts +40 -8
- package/src/clients/ai-gateway.ts +4 -0
- package/src/clients/anthropic-messages.ts +60 -0
- package/src/clients/anthropic.ts +4 -0
- package/src/clients/aws.ts +119 -0
- package/src/clients/cerebras.ts +4 -0
- package/src/clients/clerk.ts +97 -0
- package/src/clients/cloudflare.ts +80 -0
- package/src/clients/elevenlabs.ts +38 -0
- package/src/clients/gemini.ts +89 -0
- package/src/clients/github.ts +67 -0
- package/src/clients/groq.ts +4 -0
- package/src/clients/harness.ts +101 -0
- package/src/clients/merge.ts +9 -0
- package/src/clients/openai-compatible.ts +59 -0
- package/src/clients/openai.ts +66 -0
- package/src/clients/openrouter.ts +4 -0
- package/src/clients/perplexity.ts +4 -0
- package/src/clients/planetscale.ts +98 -0
- package/src/clients/posthog.ts +132 -0
- package/src/clients/run.ts +18 -0
- package/src/clients/sentry.ts +157 -0
- package/src/clients/slack.ts +98 -0
- package/src/clients/stripe.ts +129 -0
- package/src/clients/tinybird.ts +69 -0
- package/src/clients/tunnel.ts +79 -0
- package/src/clients/upstash.ts +137 -0
- package/src/clients/volteridentity.ts +108 -0
- package/src/clients/xai.ts +25 -0
- package/src/conformance.ts +446 -0
- package/src/derive.ts +87 -16
- package/src/gate.ts +20 -14
- package/src/index.ts +5 -3
- package/src/lanes.ts +7 -0
- package/src/p3-rules.ts +0 -2
- package/src/protocol-3.ts +71 -27
- package/src/published.ts +59 -11
- package/src/spec-documents.ts +2 -1
- package/src/spec-ir-client.ts +15 -4
- package/src/spec-ir.ts +43 -5
- package/src/verdict.ts +28 -0
- package/src/walk/behavior-journey.ts +806 -0
- package/src/walk/git-client.ts +138 -0
- package/src/walk/journey-kit.ts +858 -0
- package/src/walk/shape.ts +208 -0
- package/src/walk/sse-example.ts +25 -0
- package/src/walk/vendor-fetch.ts +80 -0
- package/src/walk/walk-database.ts +97 -0
- package/src/walk/xml-view.ts +44 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The chat cases every vendor whose chat API is OpenAI's wire holds, driven by openai-node pointed at the vendor (as an
|
|
2
|
+
// application using the OpenAI SDK with that vendor's base URL does): a turn answered with the World's scenario loaded,
|
|
3
|
+
// `tool_choice: none` answered with a message, the scenario rewritten between two turns deciding the second.
|
|
4
|
+
import type { ClientCase, ClientContext } from './harness.ts';
|
|
5
|
+
|
|
6
|
+
const CHAT = 'https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create';
|
|
7
|
+
const TOOLS = [{ type: 'function' as const, function: { name: 'lookup', parameters: { type: 'object', properties: {} } } }];
|
|
8
|
+
const SCRIPT = { handlers: [
|
|
9
|
+
{ id: 'calls', on: { hasTool: 'lookup' }, respond: { toolCalls: [{ name: 'lookup', arguments: {} }] } },
|
|
10
|
+
{ id: 'words', on: { userTextIncludes: 'hello' }, respond: { text: 'scripted words' } },
|
|
11
|
+
] };
|
|
12
|
+
|
|
13
|
+
/** A vendor's chat as openai-node reaches it: the variable its key is issued in, the base URL on its host, a model it serves. */
|
|
14
|
+
export type Compatible = { key: string; base: (host: string) => string; model: string };
|
|
15
|
+
|
|
16
|
+
export async function openaiClient(c: ClientContext, v: Compatible) {
|
|
17
|
+
const { default: OpenAI } = await import('openai');
|
|
18
|
+
return new OpenAI({ apiKey: c.env[v.key] ?? '', baseURL: v.base(c.host), fetch: c.fetch, maxRetries: 0, timeout: 10_000 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function chatCases(v: Compatible): ClientCase[] {
|
|
22
|
+
const ask = async (c: ClientContext, extra: Record<string, unknown> = {}) => (await openaiClient(c, v)).chat.completions.create({ model: v.model, messages: [{ role: 'user', content: 'hello' }], ...extra } as never) as Promise<{ choices?: Array<{ message?: { content?: string | null; tool_calls?: unknown[] } }> }>;
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
id: 'chat-answers',
|
|
26
|
+
asks: "a chat completion is answered, the World's scenario loaded",
|
|
27
|
+
source: `${CHAT} "Creates a model response for the given chat conversation."`,
|
|
28
|
+
async run(c) {
|
|
29
|
+
c.scenario(SCRIPT);
|
|
30
|
+
const answer = await ask(c).catch((e: Error) => e);
|
|
31
|
+
if (answer instanceof Error) return [`the client's chat.completions.create failed: ${answer.message}`];
|
|
32
|
+
return answer.choices?.[0]?.message?.content === 'scripted words' ? [] : [`the scripted turn was not answered: ${JSON.stringify(answer.choices?.[0]?.message).slice(0, 120)}`];
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'chat-tool-choice-none',
|
|
37
|
+
asks: 'tool_choice none is answered with a message, never a tool call',
|
|
38
|
+
source: `${CHAT} "none means the model will not call any tool and instead generates a message."`,
|
|
39
|
+
async run(c) {
|
|
40
|
+
c.scenario(SCRIPT);
|
|
41
|
+
const none = await ask(c, { tools: TOOLS, tool_choice: 'none' }).catch((e: Error) => e);
|
|
42
|
+
if (none instanceof Error) return [`the client's chat.completions.create failed: ${none.message}`];
|
|
43
|
+
return none.choices?.[0]?.message?.tool_calls?.length ? ['tool_choice none was answered with a tool call'] : [];
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'scenario-each-turn',
|
|
48
|
+
asks: "the World's scenario rewritten between two turns decides the second (no reboot)",
|
|
49
|
+
source: 'architecture "The scenario is read from the World\'s store at every turn"',
|
|
50
|
+
async run(c) {
|
|
51
|
+
c.scenario({ handlers: [{ id: 'one', on: { userTextIncludes: 'hello' }, respond: { text: 'first script' } }] });
|
|
52
|
+
const first = await ask(c).then((a) => a.choices?.[0]?.message?.content, (e: Error) => `error ${e.message}`);
|
|
53
|
+
c.scenario({ handlers: [{ id: 'two', on: { userTextIncludes: 'hello' }, respond: { text: 'second script' } }] });
|
|
54
|
+
const second = await ask(c).then((a) => a.choices?.[0]?.message?.content, (e: Error) => `error ${e.message}`);
|
|
55
|
+
return first === 'first script' && second === 'second script' ? [] : [`the turns answered "${first}" then "${second}"`];
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// OpenAI's own client (openai-node), driving the openai pack as an application does. Each case is what OpenAI documents;
|
|
2
|
+
// the World's scenario scripts the turns a model would take.
|
|
3
|
+
import type { ClientCase, ClientContext } from './harness.ts';
|
|
4
|
+
import { chatCases } from './openai-compatible.ts';
|
|
5
|
+
|
|
6
|
+
const CHAT = 'https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create';
|
|
7
|
+
const REALTIME = 'https://developers.openai.com/api/reference/resources/realtime/server-events';
|
|
8
|
+
|
|
9
|
+
async function client(c: ClientContext) {
|
|
10
|
+
const { default: OpenAI } = await import('openai');
|
|
11
|
+
return new OpenAI({ apiKey: c.env.OPENAI_API_KEY ?? '', baseURL: `https://${c.host}/v1`, fetch: c.fetch, maxRetries: 0, timeout: 10_000 });
|
|
12
|
+
}
|
|
13
|
+
const TOOLS = [{ type: 'function' as const, function: { name: 'lookup', parameters: { type: 'object', properties: {} } } }];
|
|
14
|
+
// a turn that calls the tool wherever one is offered, and one that answers in words
|
|
15
|
+
const SCRIPT = { handlers: [
|
|
16
|
+
{ id: 'calls', on: { hasTool: 'lookup' }, respond: { toolCalls: [{ name: 'lookup', arguments: {} }] } },
|
|
17
|
+
{ id: 'words', on: { userTextIncludes: 'hello' }, respond: { text: 'scripted words' } },
|
|
18
|
+
] };
|
|
19
|
+
|
|
20
|
+
/** A Realtime session through the client (openai-node's server-side Realtime client, on the ws library, as a Node
|
|
21
|
+
* application opens it), until its response is done: its first event and the output items' types, or what went wrong. */
|
|
22
|
+
async function realtimeTurn(c: ClientContext, toolChoice: 'auto' | 'none'): Promise<{ first?: string; output?: string[]; error?: string }> {
|
|
23
|
+
const { OpenAIRealtimeWS } = await import('openai/realtime/ws');
|
|
24
|
+
const openai = await client(c);
|
|
25
|
+
return new Promise((done) => {
|
|
26
|
+
const rt = new OpenAIRealtimeWS({ model: 'gpt-realtime', options: { createConnection: c.connect } as never }, openai);
|
|
27
|
+
let first: string | undefined;
|
|
28
|
+
const timer = setTimeout(() => { rt.close(); done({ first, error: 'no response.done in 10 s' }); }, 10_000);
|
|
29
|
+
rt.on('error', (e) => { clearTimeout(timer); rt.close(); done({ first, error: String((e as Error).message ?? e) }); });
|
|
30
|
+
rt.socket.on('open', () => {
|
|
31
|
+
rt.send({ type: 'session.update', session: { type: 'realtime', output_modalities: ['text'], tools: [{ type: 'function', name: 'lookup', parameters: { type: 'object', properties: {} } }], tool_choice: toolChoice } } as never);
|
|
32
|
+
rt.send({ type: 'conversation.item.create', item: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'hello' }] } } as never);
|
|
33
|
+
rt.send({ type: 'response.create' } as never);
|
|
34
|
+
});
|
|
35
|
+
rt.on('event', (e: { type: string; response?: { output?: Array<{ type: string }> } }) => {
|
|
36
|
+
first ??= e.type;
|
|
37
|
+
if (e.type === 'response.done') { clearTimeout(timer); rt.close(); done({ first, output: (e.response?.output ?? []).map((o) => o.type) }); }
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const cases: ClientCase[] = [
|
|
43
|
+
...chatCases({ key: 'OPENAI_API_KEY', base: (host) => `https://${host}/v1`, model: 'gpt-4o' }),
|
|
44
|
+
{
|
|
45
|
+
id: 'realtime-connects',
|
|
46
|
+
asks: 'a Realtime connection opens and its first event is session.created',
|
|
47
|
+
source: `${REALTIME} "Emitted automatically when a new connection is established as the first server event."`,
|
|
48
|
+
async run(c) {
|
|
49
|
+
c.scenario(SCRIPT);
|
|
50
|
+
const turn = await realtimeTurn(c, 'auto');
|
|
51
|
+
if (turn.error && !turn.first) return [`the client's Realtime connection failed: ${turn.error}`];
|
|
52
|
+
return turn.first === 'session.created' ? [] : [`the first server event was ${turn.first ?? 'none'}`];
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'realtime-tool-choice-none',
|
|
57
|
+
asks: "a Realtime response under tool_choice none carries no function call",
|
|
58
|
+
source: `${CHAT} "none means the model will not call any tool and instead generates a message."`,
|
|
59
|
+
async run(c) {
|
|
60
|
+
c.scenario(SCRIPT);
|
|
61
|
+
const turn = await realtimeTurn(c, 'none');
|
|
62
|
+
if (turn.error) return [`the Realtime turn failed: ${turn.error}`];
|
|
63
|
+
return turn.output?.includes('function_call') ? ['tool_choice none was answered with a function_call item'] : [];
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// OpenRouter's chat through openai-node (its OpenAI-compatible API), the shared chat cases.
|
|
2
|
+
import { chatCases } from './openai-compatible.ts';
|
|
3
|
+
|
|
4
|
+
export const cases = chatCases({ key: 'OPENROUTER_API_KEY', base: (host) => `https://${host}/api/v1`, model: 'openai/gpt-4o' });
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// PlanetScale's own driver (@planetscale/database, at the version Dub and Substrate pin), driving the planetscale pack as
|
|
2
|
+
// its applications do, with the connection string the pack's credential door issued (PLANETSCALE_DATABASE_URL): Dub's
|
|
3
|
+
// connection sends SQL with `?` parameters and reads typed rows; Prisma's PlanetScale adapter (Dub's edge client and the
|
|
4
|
+
// client the World's injector gives Dub's PrismaClient, Substrate's Prisma client in a tab) reads rows as arrays, runs an
|
|
5
|
+
// interactive transaction through the driver's `transaction`, and reads MySQL's errno and sqlstate out of a
|
|
6
|
+
// `DatabaseError`'s message (`(errno 1062) (sqlstate 23000)` is Prisma's unique-constraint violation).
|
|
7
|
+
import type { ClientCase, ClientContext } from './harness.ts';
|
|
8
|
+
|
|
9
|
+
const DRIVER = 'https://planetscale.com/docs/tutorials/planetscale-serverless-driver';
|
|
10
|
+
const README = 'https://github.com/planetscale/database-js';
|
|
11
|
+
const ERRORS = 'https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html';
|
|
12
|
+
|
|
13
|
+
type Connection = {
|
|
14
|
+
execute: (query: string, args?: unknown, options?: { as?: 'object' | 'array' }) => Promise<{ rows: unknown[]; types: Record<string, string>; rowsAffected: number; insertId: string; statement: string }>;
|
|
15
|
+
transaction: <T>(fn: (tx: { execute: Connection['execute'] }) => Promise<T>) => Promise<T>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** A connection as Dub's lib/planetscale/connection.ts makes one: `connect({ url })`, here with the World's fetch. */
|
|
19
|
+
async function connection(c: ClientContext): Promise<Connection> {
|
|
20
|
+
const { connect } = (await import('@planetscale/database')) as unknown as { connect: (config: { url?: string; fetch?: typeof fetch }) => Connection };
|
|
21
|
+
return connect({ url: c.env.PLANETSCALE_DATABASE_URL, fetch: c.fetch });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const cases: ClientCase[] = [
|
|
25
|
+
{
|
|
26
|
+
id: 'parameters-and-types',
|
|
27
|
+
asks: "SQL sent with ? parameters, each replaced by its value, and the rows read back as the column's JavaScript type, as objects and as arrays",
|
|
28
|
+
source: `${DRIVER} "The driver package will replace the ? entries in the query with the values passed in the array, in the order in which they were placed."; ${README} "Column values are converted to their corresponding JavaScript data types."; ${README} "Rows can be returned as an object or an array of column values by passing an as option to execute ."`,
|
|
29
|
+
async run(c) {
|
|
30
|
+
try {
|
|
31
|
+
if (!c.env.PLANETSCALE_DATABASE_URL) return ['the credential door issued no PLANETSCALE_DATABASE_URL'];
|
|
32
|
+
const conn = await connection(c);
|
|
33
|
+
await conn.execute('CREATE TABLE hotels (id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, address VARCHAR(200) NOT NULL, stars FLOAT NOT NULL, PRIMARY KEY (id))');
|
|
34
|
+
const out: string[] = [];
|
|
35
|
+
const inserted = await conn.execute('INSERT INTO hotels (`name`, `address`, `stars`) VALUES (?, ?, ?)', ['The Galt House', '140 N Fourth St, Louisville, KY 40202', 4.2]);
|
|
36
|
+
if (inserted.rowsAffected !== 1) out.push(`the insert affected ${inserted.rowsAffected} rows, not 1`);
|
|
37
|
+
if (inserted.insertId !== '1') out.push(`the insert's id is ${JSON.stringify(inserted.insertId)}, not "1"`);
|
|
38
|
+
if (!inserted.statement.includes("'The Galt House'")) out.push(`the statement sent was ${JSON.stringify(inserted.statement)}`);
|
|
39
|
+
const read = await conn.execute('SELECT id, name, stars FROM hotels WHERE name = ?', ['The Galt House']);
|
|
40
|
+
const row = read.rows[0] as Record<string, unknown> | undefined;
|
|
41
|
+
if (!row) return [...out, 'the hotel inserted was not read back'];
|
|
42
|
+
if (row.id !== 1) out.push(`its id read back as ${JSON.stringify(row.id)}, not the number 1`);
|
|
43
|
+
if (row.name !== 'The Galt House') out.push(`its name read back as ${JSON.stringify(row.name)}`);
|
|
44
|
+
if (row.stars !== 4.2) out.push(`its stars read back as ${JSON.stringify(row.stars)} (${read.types.stars}), not the number 4.2`);
|
|
45
|
+
if (read.types.id !== 'UINT32') out.push(`its id's type is ${String(read.types.id)}, not UINT32`);
|
|
46
|
+
const arrays = await conn.execute('SELECT id, name FROM hotels', null, { as: 'array' });
|
|
47
|
+
if (JSON.stringify(arrays.rows) !== JSON.stringify([[1, 'The Galt House']])) out.push(`as arrays the rows are ${JSON.stringify(arrays.rows)}`);
|
|
48
|
+
return out;
|
|
49
|
+
} catch (e) { return [`the driver's calls failed: ${(e as Error).name}: ${(e as Error).message}`]; }
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'transaction-rolls-back',
|
|
54
|
+
asks: 'a transaction whose callback returns is committed, and one whose callback throws is rolled back, its writes gone',
|
|
55
|
+
source: `${README} "Use the transaction function to safely perform database transactions. If any unhandled errors are thrown during execution of the transaction, the transaction will be rolled back."`,
|
|
56
|
+
async run(c) {
|
|
57
|
+
try {
|
|
58
|
+
if (!c.env.PLANETSCALE_DATABASE_URL) return ['the credential door issued no PLANETSCALE_DATABASE_URL'];
|
|
59
|
+
const conn = await connection(c);
|
|
60
|
+
await conn.execute('CREATE TABLE slotted_counters (record_type VARCHAR(40) NOT NULL, record_id INT NOT NULL, count INT NOT NULL, PRIMARY KEY (record_type, record_id))');
|
|
61
|
+
await conn.transaction(async (tx) => {
|
|
62
|
+
await tx.execute("INSERT INTO slotted_counters (record_type, record_id, count) VALUES ('branch_count', 42, 1)");
|
|
63
|
+
await tx.execute("INSERT INTO slotted_counters (record_type, record_id, count) VALUES ('branch_count', 43, 1)");
|
|
64
|
+
});
|
|
65
|
+
const refused = new Error('the second branch was refused');
|
|
66
|
+
const raised = await conn.transaction(async (tx) => {
|
|
67
|
+
await tx.execute("UPDATE slotted_counters SET count = count + 1 WHERE record_id = 42");
|
|
68
|
+
throw refused;
|
|
69
|
+
}).then(() => undefined, (e: unknown) => e);
|
|
70
|
+
const out: string[] = [];
|
|
71
|
+
if (raised !== refused) out.push(`the failed transaction raised ${String((raised as Error | undefined)?.message)}, not its callback's error`);
|
|
72
|
+
const after = await conn.execute('SELECT record_id, count FROM slotted_counters ORDER BY record_id');
|
|
73
|
+
if (JSON.stringify(after.rows) !== JSON.stringify([{ record_id: 42, count: 1 }, { record_id: 43, count: 1 }])) out.push(`after a commit and a rollback the counters are ${JSON.stringify(after.rows)}, not both at 1`);
|
|
74
|
+
return out;
|
|
75
|
+
} catch (e) { return [`the driver's calls failed: ${(e as Error).name}: ${(e as Error).message}`]; }
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'duplicate-key-raises',
|
|
80
|
+
asks: "inserting a primary key the table holds raises the driver's DatabaseError, its message ending in MySQL's errno and sqlstate, (errno 1062) (sqlstate 23000)",
|
|
81
|
+
source: `${ERRORS} "ER_DUP_ENTRY"; ${ERRORS} "Duplicate entry '%s' for key '%s'"`,
|
|
82
|
+
async run(c) {
|
|
83
|
+
try {
|
|
84
|
+
if (!c.env.PLANETSCALE_DATABASE_URL) return ['the credential door issued no PLANETSCALE_DATABASE_URL'];
|
|
85
|
+
const conn = await connection(c);
|
|
86
|
+
await conn.execute('CREATE TABLE links (domain VARCHAR(190) NOT NULL, `key` VARCHAR(190) NOT NULL, PRIMARY KEY (domain, `key`))');
|
|
87
|
+
await conn.execute('INSERT INTO links (domain, `key`) VALUES (?, ?)', ['go.trailmark.io', 'pricing']);
|
|
88
|
+
const raised = await conn.execute('INSERT INTO links (domain, `key`) VALUES (?, ?)', ['go.trailmark.io', 'pricing'])
|
|
89
|
+
.then(() => undefined, (e: unknown) => e as { name?: string; message?: string });
|
|
90
|
+
if (!raised) return ['inserting the same key twice succeeded'];
|
|
91
|
+
const out: string[] = [];
|
|
92
|
+
if (raised.name !== 'DatabaseError') out.push(`it raised ${String(raised.name)}, not the driver's DatabaseError`);
|
|
93
|
+
if (!/ \(errno 1062\) \(sqlstate 23000\)$/.test(String(raised.message))) out.push(`its message is ${JSON.stringify(raised.message)}, not MySQL's errno 1062 and sqlstate 23000`);
|
|
94
|
+
return out;
|
|
95
|
+
} catch (e) { return [`the driver's calls failed: ${(e as Error).name}: ${(e as Error).message}`]; }
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
];
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Official posthog-node 5.46.1 and @posthog/core 1.45.1, the versions Rallly pins.
|
|
2
|
+
// The client's fetch option keeps every forwarded request on the World's proxy.
|
|
3
|
+
import type { ClientCase, ClientContext } from './harness.ts';
|
|
4
|
+
|
|
5
|
+
const NODE = 'https://posthog.com/docs/libraries/node';
|
|
6
|
+
const CORE = 'https://unpkg.com/@posthog/core@1.45.1/src/posthog-core-stateless.ts';
|
|
7
|
+
const IDENTIFY = 'https://posthog.com/docs/product-analytics/person-properties';
|
|
8
|
+
|
|
9
|
+
async function client(c: ClientContext, options: { key?: string; fetch?: typeof fetch; flushAt?: number } = {}) {
|
|
10
|
+
const { PostHog } = await import('posthog-node');
|
|
11
|
+
return new PostHog(options.key ?? c.env.POSTHOG_KEY ?? '', {
|
|
12
|
+
host: 'https://' + c.host, fetch: options.fetch ?? c.fetch, flushAt: options.flushAt ?? 100,
|
|
13
|
+
flushInterval: 0, fetchRetryCount: 2, fetchRetryDelay: 1, requestTimeout: 10_000,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
async function read(c: ClientContext, resource: string, query = ''): Promise<Record<string, any>> {
|
|
17
|
+
const response = await c.fetch((c.env.POSTHOG_API_HOST || 'https://us.posthog.com') + '/api/projects/' + c.env.POSTHOG_PROJECT_ID + '/' + resource + '/' + query, {
|
|
18
|
+
headers: { authorization: 'Bearer ' + c.env.POSTHOG_PERSONAL_API_KEY },
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) throw new Error(resource + ' readback answered ' + response.status + ': ' + await response.text());
|
|
21
|
+
return await response.json() as Record<string, any>;
|
|
22
|
+
}
|
|
23
|
+
export const cases: ClientCase[] = [
|
|
24
|
+
{
|
|
25
|
+
id: 'queued-captures-and-groups-drained-on-shutdown',
|
|
26
|
+
asks: 'shutdown drains queued captures and groupIdentify into a compressed batch; events and group properties read back',
|
|
27
|
+
source: NODE + ' "which can be awaited to ensure all queued events are sent to the API"',
|
|
28
|
+
async run(c) {
|
|
29
|
+
const requests: Request[] = [];
|
|
30
|
+
const forwarded = (async (url: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
|
31
|
+
const request = new Request(url, init);
|
|
32
|
+
if (request.method === 'POST') requests.push(request.clone());
|
|
33
|
+
return c.fetch(request);
|
|
34
|
+
}) as typeof fetch;
|
|
35
|
+
const sdk = await client(c, { fetch: forwarded });
|
|
36
|
+
sdk.groupIdentify({ groupType: 'organization', groupKey: 'sdk-harbourline', distinctId: 'sdk-manager', properties: { name: 'Harbourline SDK case' } });
|
|
37
|
+
sdk.capture({ distinctId: 'sdk-manager', event: 'sdk_meeting_started', properties: { meeting: 'morning' }, groups: { organization: 'sdk-harbourline' } });
|
|
38
|
+
sdk.capture({ distinctId: 'sdk-manager', event: 'sdk_meeting_finished', properties: { meeting: 'morning' }, groups: { organization: 'sdk-harbourline' } });
|
|
39
|
+
await sdk.shutdown();
|
|
40
|
+
const failures: string[] = [];
|
|
41
|
+
if (requests.length !== 1 || !new URL(requests[0]!.url).pathname.startsWith('/batch')) failures.push('three queued calls did not drain as one batch');
|
|
42
|
+
if (requests[0]?.headers.get('content-encoding') !== 'gzip') failures.push('the default Node transport did not send gzip');
|
|
43
|
+
const events = await read(c, 'events', '?distinct_id=sdk-manager');
|
|
44
|
+
for (const event of ['sdk_meeting_started', 'sdk_meeting_finished']) {
|
|
45
|
+
const row = events.results.find((r: any) => r.event === event);
|
|
46
|
+
if (!row || row.properties.meeting !== 'morning' || row.properties.$groups.organization !== 'sdk-harbourline') failures.push(event + ' lost its properties or group link');
|
|
47
|
+
}
|
|
48
|
+
const types = await read(c, 'groups_types');
|
|
49
|
+
const groupType = (Array.isArray(types) ? types : types.results).find((r: any) => r.group_type === 'organization');
|
|
50
|
+
if (!groupType) failures.push('groupIdentify made no group type');
|
|
51
|
+
else {
|
|
52
|
+
const groups = await read(c, 'groups', '?group_type_index=' + groupType.group_type_index + '&group_key=sdk-harbourline');
|
|
53
|
+
if (groups.results[0]?.group_properties.name !== 'Harbourline SDK case') failures.push('groupIdentify lost the group properties');
|
|
54
|
+
}
|
|
55
|
+
return failures;
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'identify-alias-and-person-properties',
|
|
60
|
+
asks: 'alias links an anonymous ID; set_once preserves its first value and unset removes the named profile property',
|
|
61
|
+
source: IDENTIFY + ' "set_once only sets the property if it has never been set before."',
|
|
62
|
+
async run(c) {
|
|
63
|
+
const sdk = await client(c);
|
|
64
|
+
sdk.capture({ distinctId: 'sdk-anonymous', event: 'sdk_pageview', properties: { $set: { source: 'invite' } } });
|
|
65
|
+
sdk.alias({ distinctId: 'sdk-member', alias: 'sdk-anonymous' });
|
|
66
|
+
sdk.identify({ distinctId: 'sdk-member', properties: { email: 'sdk@harbourline.test' } });
|
|
67
|
+
sdk.setPersonProperties({ distinctId: 'sdk-member', properties: { plan: 'trial' }, propertiesOnce: { first_plan: 'trial' } });
|
|
68
|
+
sdk.setPersonProperties({ distinctId: 'sdk-member', properties: { plan: 'paid' }, propertiesOnce: { first_plan: 'paid' } });
|
|
69
|
+
sdk.unsetPersonProperties({ distinctId: 'sdk-member', properties: ['plan'] });
|
|
70
|
+
await sdk.flush();
|
|
71
|
+
await sdk.shutdown();
|
|
72
|
+
const rows = (await read(c, 'persons', '?distinct_id=sdk-member')).results;
|
|
73
|
+
const person = rows[0];
|
|
74
|
+
if (rows.length !== 1 || !person.distinct_ids.includes('sdk-anonymous')) return ['the alias did not link the two IDs to one person'];
|
|
75
|
+
return person.properties.source === 'invite' && person.properties.email === 'sdk@harbourline.test'
|
|
76
|
+
&& person.properties.first_plan === 'trial' && !('plan' in person.properties) ? [] : ['profile updates lost the anonymous properties, overwrote set_once, or retained the unset property'];
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'transient-retry-preserves-request-and-event-uuid',
|
|
81
|
+
asks: 'a transient503 is retried with the same event UUID and accepted once by the twin',
|
|
82
|
+
source: CORE + ' "return err.status === 408 || err.status === 429 || err.status >= 500"',
|
|
83
|
+
async run(c) {
|
|
84
|
+
const requests: Request[] = [];
|
|
85
|
+
const forwarded = (async (url: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
|
86
|
+
const request = new Request(url, init);
|
|
87
|
+
if (request.method !== 'POST') return c.fetch(request);
|
|
88
|
+
requests.push(request.clone());
|
|
89
|
+
if (requests.length === 1) return new Response('service unavailable', { status: 503 });
|
|
90
|
+
return c.fetch(request);
|
|
91
|
+
}) as typeof fetch;
|
|
92
|
+
const sdk = await client(c, { fetch: forwarded });
|
|
93
|
+
await sdk.captureImmediate({ distinctId: 'sdk-retry-member', event: 'sdk_retry_capture' });
|
|
94
|
+
await sdk.shutdown();
|
|
95
|
+
const failures: string[] = [];
|
|
96
|
+
if (requests.length !== 2) failures.push('one transient503 did not cause exactly one retry');
|
|
97
|
+
else {
|
|
98
|
+
const first = new Uint8Array(await requests[0]!.arrayBuffer()), second = new Uint8Array(await requests[1]!.arrayBuffer());
|
|
99
|
+
if (first.length !== second.length || first.some((v, i) => v !== second[i])) failures.push('the retry changed the prepared payload and event UUID');
|
|
100
|
+
}
|
|
101
|
+
const events = await read(c, 'events', '?event=sdk_retry_capture');
|
|
102
|
+
if (events.results.length !== 1 || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(events.results[0]?.id ?? '')) failures.push('the retried capture is not one stored event with a UUID');
|
|
103
|
+
return failures;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'nonretryable401-is-a-typed-http-error',
|
|
108
|
+
asks: 'capture with a personal key is rejected as401 once; the official client exposes PostHogFetchHttpError and its status',
|
|
109
|
+
source: CORE + ' "class PostHogFetchHttpError extends Error"',
|
|
110
|
+
async run(c) {
|
|
111
|
+
let attempts = 0;
|
|
112
|
+
const sdk = await client(c, { key: c.env.POSTHOG_PERSONAL_API_KEY, fetch: (async (url: Parameters<typeof fetch>[0], init?: RequestInit) => { attempts++; return c.fetch(url, init); }) as typeof fetch });
|
|
113
|
+
let error: unknown;
|
|
114
|
+
try { await sdk.captureImmediate({ distinctId: 'sdk-refusal', event: 'sdk_refused_capture' }); } catch (caught) { error = caught; }
|
|
115
|
+
await sdk.shutdown();
|
|
116
|
+
const typed = error as { name?: string; status?: number } | undefined;
|
|
117
|
+
return attempts === 1 && typed?.name === 'PostHogFetchHttpError' && typed.status === 401 ? [] : ['the401 was retried or did not expose the SDK HTTP error and status'];
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'flags-empty-account-through-node-client',
|
|
122
|
+
asks: 'getAllFlags interprets the v2 response with person/group overrides and requested keys for this account with no flags',
|
|
123
|
+
source: 'https://unpkg.com/posthog-node@5.46.1/src/client.ts "Promise that resolves to a record of flag keys and their values"',
|
|
124
|
+
async run(c) {
|
|
125
|
+
const sdk = await client(c);
|
|
126
|
+
const flags = await sdk.getAllFlags('sdk-member', { groups: { organization: 'sdk-harbourline' },
|
|
127
|
+
personProperties: { email: 'sdk@harbourline.test' }, groupProperties: { organization: { name: 'Harbourline SDK case' } }, flagKeys: ['new-booking-flow'] });
|
|
128
|
+
await sdk.shutdown();
|
|
129
|
+
return Object.keys(flags).length === 0 ? [] : ['the empty account returned invented flags'];
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The vendor's client cases, run in Node as an application runs its SDK: `node run.ts <job.json>`. The job names the
|
|
2
|
+
// vendor, the served port, the host, the issued credentials and the scenario file; each case's failures are printed as
|
|
3
|
+
// one JSON line, `{ id, asks, failures }`.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { clientContext, worldProxy, type ClientCase } from './harness.ts';
|
|
6
|
+
|
|
7
|
+
const job = JSON.parse(readFileSync(process.argv[2]!, 'utf8')) as { vendor: string; port: number; host: string; env: Record<string, string>; scenarioPath: string };
|
|
8
|
+
const { cases } = (await import(`./${job.vendor}.ts`)) as { cases: ClientCase[] };
|
|
9
|
+
// every connection a case makes goes through the World's proxy, as an application's does in a World
|
|
10
|
+
const context = clientContext(await worldProxy(job.port), job.host, job.env, job.scenarioPath);
|
|
11
|
+
for (const c of cases) {
|
|
12
|
+
const failures = await Promise.race([
|
|
13
|
+
c.run(context).catch((e: Error) => [`the case threw: ${e.message}`]),
|
|
14
|
+
new Promise<string[]>((r) => setTimeout(() => r(['no outcome in 30 s']), 30_000)),
|
|
15
|
+
]);
|
|
16
|
+
process.stdout.write(`${JSON.stringify({ id: c.id, asks: c.asks, failures })}\n`);
|
|
17
|
+
}
|
|
18
|
+
process.exit(0);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Official SDKs at the demanded applications' pinned versions. The native HTTP module
|
|
2
|
+
// takes the harness agent, preserving the SDK's DSN URL, compression, queue and wire.
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { request as httpsRequest } from 'node:https';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import type { ClientCase, ClientContext } from './harness.ts';
|
|
9
|
+
|
|
10
|
+
// The pins live in ../../client-sdks with their own lockfile, installed here on first use: some carry advisories with no
|
|
11
|
+
// fix in the line an application pins, so the repository's install (and a release, .github/release-scan.mjs) never does.
|
|
12
|
+
const SDKS = new URL('../../client-sdks/', import.meta.url);
|
|
13
|
+
async function sdkOf(name: string): Promise<any> {
|
|
14
|
+
if (!existsSync(new URL('node_modules/', SDKS))) {
|
|
15
|
+
const install = spawnSync('bun', ['install', '--frozen-lockfile', '--ignore-scripts'], { cwd: SDKS, stdio: 'inherit' });
|
|
16
|
+
if (install.status !== 0) throw new Error('client-sdks: bun install --frozen-lockfile failed');
|
|
17
|
+
}
|
|
18
|
+
return await import(pathToFileURL(createRequire(new URL('package.json', SDKS)).resolve(name)).href);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const pins = [
|
|
22
|
+
['rallly', '@sentry/node', '10.38.0'],
|
|
23
|
+
['twenty', 'sentry-node-twenty', '10.73.0'],
|
|
24
|
+
['postiz', 'sentry-node-postiz', '10.56.0'],
|
|
25
|
+
['cal-api', 'sentry-node-cal', '9.46.0'],
|
|
26
|
+
['cal-web', 'sentry-node-cal-web', '10.33.0'],
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
async function token(c: ClientContext): Promise<string> {
|
|
30
|
+
const answer = await c.fetch('https://' + c.host + '/_twin/auth-tokens', { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({ organization: c.env.SENTRY_ORG, name: 'SDK read-back', kind: 'personal', scopes: ['org:read', 'project:read', 'event:read'] }) });
|
|
32
|
+
if (!answer.ok) throw new Error('read-back token: ' + answer.status);
|
|
33
|
+
return (await answer.json() as { token: string }).token;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function read(c: ClientContext, id: string, auth: string): Promise<Record<string, any>> {
|
|
37
|
+
const answer = await c.fetch(`https://${c.host}/api/0/projects/${c.env.SENTRY_ORG}/${c.env.SENTRY_PROJECT}/events/${id}/`, { headers: { authorization: 'Bearer ' + auth } });
|
|
38
|
+
if (!answer.ok) throw new Error('event read-back: ' + answer.status);
|
|
39
|
+
return await answer.json();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const cases: ClientCase[] = pins.map(([application, name, version]) => ({
|
|
43
|
+
id: application + '-native-exception-flush-compression',
|
|
44
|
+
asks: 'captureException prepares its event ID, stack and scope; flush drains the native compressed envelope and its event reads back',
|
|
45
|
+
source: `https://unpkg.com/@sentry/core@${version}/build/esm/client.js "transport.flush(timeout)"`,
|
|
46
|
+
async run(c) {
|
|
47
|
+
const sdk = await sdkOf(name);
|
|
48
|
+
const requests: Array<{ url: string; encoding?: string }> = [];
|
|
49
|
+
const client = new sdk.NodeClient({ dsn: c.env.SENTRY_DSN, integrations: [], stackParser: sdk.defaultStackParser,
|
|
50
|
+
release: 'sdk-' + application, environment: 'sdk', transport: (options: any) => sdk.makeNodeTransport({ ...options,
|
|
51
|
+
httpModule: { request(options: any, callback: any) { requests.push({ url: String(options.path), encoding: options.headers?.['Content-Encoding'] ?? options.headers?.['content-encoding'] }); return httpsRequest({ ...options, agent: c.agent }, callback); } },
|
|
52
|
+
}) });
|
|
53
|
+
const scope = new sdk.Scope();
|
|
54
|
+
scope.setUser({ id: 'sdk-member' }); scope.setTag('application', application);
|
|
55
|
+
scope.setExtra('large-context', 'x'.repeat(64 * 1024));
|
|
56
|
+
const id = client.captureException(new Error('SDK ' + application + ' exception'), undefined, scope);
|
|
57
|
+
const drained = await client.flush(10_000);
|
|
58
|
+
const event = await read(c, id, await token(c));
|
|
59
|
+
await client.close(10_000);
|
|
60
|
+
const failures: string[] = [];
|
|
61
|
+
if (!drained || requests.length !== 1 || !requests[0]!.url.includes('/envelope/')) failures.push('flush did not drain exactly one exception envelope');
|
|
62
|
+
if (requests[0]?.encoding !== 'gzip') failures.push('large Node envelope was not compressed');
|
|
63
|
+
if (event.eventID !== id || event.user?.id !== 'sdk-member' || event.context?.['large-context']?.length !== 64 * 1024) failures.push('event ID or scope did not survive the native transport');
|
|
64
|
+
if (!event.entries?.some((e: any) => e.type === 'exception' && e.data.values[0]?.stacktrace?.frames?.length)) failures.push('the SDK stack was lost');
|
|
65
|
+
return failures;
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
for (const [name, version] of [['@sentry/browser', '10.38.0'], ['sentry-browser-twenty', '10.59.0'], ['sentry-browser-postiz', '10.56.0']] as const) cases.push({
|
|
70
|
+
id: 'browser-' + version + '-dsn-query-auth-and-session-envelope',
|
|
71
|
+
asks: 'the browser fetch transport uses DSN query authentication and flushes event and session envelopes',
|
|
72
|
+
source: `https://unpkg.com/@sentry/core@${version}/build/esm/api.js "sentry_key: publicKey"`,
|
|
73
|
+
async run(c) {
|
|
74
|
+
const sdk = await sdkOf(name);
|
|
75
|
+
const { makeSession } = await sdkOf('@sentry/core');
|
|
76
|
+
const sent: Request[] = [];
|
|
77
|
+
const forward = (async (url: Parameters<typeof fetch>[0], init?: RequestInit) => { const request = new Request(url, init); sent.push(request.clone()); return c.fetch(request); }) as typeof fetch;
|
|
78
|
+
const client = new sdk.BrowserClient({ dsn: c.env.SENTRY_DSN, integrations: [], stackParser: sdk.defaultStackParser,
|
|
79
|
+
transport: (options: any) => sdk.makeFetchTransport(options, forward) });
|
|
80
|
+
const id = client.captureEvent({ message: 'browser SDK case', level: 'error', platform: 'javascript' });
|
|
81
|
+
client.captureSession(makeSession({ release: 'sdk-browser', environment: 'sdk' }));
|
|
82
|
+
const drained = await client.flush(10_000);
|
|
83
|
+
const event = await read(c, id, await token(c));
|
|
84
|
+
await client.close(10_000);
|
|
85
|
+
return drained && sent.length === 2 && sent.every((r) => new URL(r.url).searchParams.has('sentry_key')) && event.message === 'browser SDK case' ? [] : ['browser queue, DSN query authentication or event/session envelopes failed'];
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
cases.push({
|
|
90
|
+
id: 'twenty-cron-check-in-options-and-id',
|
|
91
|
+
asks: 'captureCheckIn creates an ID for in_progress and reuses it for ok with the monitor schedule and configured margins',
|
|
92
|
+
source: 'https://unpkg.com/@sentry/core@10.73.0/build/esm/server-runtime-client.js "checkin_margin: monitorConfig.checkinMargin"',
|
|
93
|
+
async run(c) {
|
|
94
|
+
const sdk = await sdkOf('sentry-node-twenty');
|
|
95
|
+
const sent: string[] = [];
|
|
96
|
+
const client = new sdk.NodeClient({ dsn: c.env.SENTRY_DSN, integrations: [], stackParser: sdk.defaultStackParser,
|
|
97
|
+
transport: (options: any) => sdk.createTransport(options, async (request: { body: string | Uint8Array }) => {
|
|
98
|
+
sent.push(typeof request.body === 'string' ? request.body : new TextDecoder().decode(request.body));
|
|
99
|
+
const answer = await c.fetch(options.url, { method: 'POST', body: request.body as BodyInit });
|
|
100
|
+
return { statusCode: answer.status, headers: {} };
|
|
101
|
+
}) });
|
|
102
|
+
let id = '';
|
|
103
|
+
sdk.withScope((scope: any) => { scope.setClient(client); id = sdk.captureCheckIn({ monitorSlug: 'sdk-cleanup', status: 'in_progress' }, { schedule: { type: 'crontab', value: '0 3 * * *' }, checkinMargin: 1, maxRuntime: 5 }); sdk.captureCheckIn({ monitorSlug: 'sdk-cleanup', status: 'ok', checkInId: id }); });
|
|
104
|
+
const drained = await client.flush(10_000); await client.close(10_000);
|
|
105
|
+
const payloads = sent.map((s) => JSON.parse(s.split('\n')[2]!));
|
|
106
|
+
return drained && payloads.length === 2 && payloads[0].check_in_id === id && payloads[1].check_in_id === id && payloads[0].monitor_config.checkin_margin === 1 && payloads[0].monitor_config.max_runtime === 5 && !('duration' in payloads[1]) ? [] : ['check-in ID or monitor options changed'];
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
cases.push({
|
|
111
|
+
id: 'transport-rate-limit-drop-and-no-retry',
|
|
112
|
+
asks: 'core transport drains its queue, drops a rate-limited category and does not retry a 503',
|
|
113
|
+
source: 'https://unpkg.com/@sentry/core@10.38.0/build/esm/transports/base.js "ratelimit_backoff"',
|
|
114
|
+
async run(c) {
|
|
115
|
+
const sdk = await sdkOf('@sentry/core');
|
|
116
|
+
const ds = new URL(c.env.SENTRY_DSN!);
|
|
117
|
+
const url = `https://${ds.host}/api${ds.pathname}/envelope/?sentry_key=${ds.username}&sentry_version=7`;
|
|
118
|
+
let attempts = 0; const dropped: string[] = [];
|
|
119
|
+
const transport = sdk.createTransport({ recordDroppedEvent: (reason: string) => { dropped.push(reason); } }, async (request: { body: string | Uint8Array }) => {
|
|
120
|
+
attempts++;
|
|
121
|
+
if (attempts === 1) return { statusCode: 503, headers: {} };
|
|
122
|
+
const response = await c.fetch(url, { method: 'POST', body: request.body as BodyInit });
|
|
123
|
+
return { statusCode: response.status, headers: { 'x-sentry-rate-limits': '60:error:organization' } };
|
|
124
|
+
});
|
|
125
|
+
const envelope = (id: string) => sdk.createEnvelope({ event_id: id }, [[{ type: 'event' }, { event_id: id, message: 'transport SDK case', platform: 'javascript' }]]);
|
|
126
|
+
await transport.send(envelope('00000000000000000000000000000001'));
|
|
127
|
+
await transport.send(envelope('00000000000000000000000000000002'));
|
|
128
|
+
await transport.send(envelope('00000000000000000000000000000003'));
|
|
129
|
+
const drained = await transport.flush(10_000);
|
|
130
|
+
return drained && attempts === 2 && dropped.includes('ratelimit_backoff') ? [] : ['transport retried 503, failed to drain, or sent a rate-limited event'];
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
cases.push({
|
|
136
|
+
id: 'postiz-feedback-attribution',
|
|
137
|
+
asks: 'captureFeedback preserves the widget message, explicit name/email, associated event and scoped user',
|
|
138
|
+
source: 'https://unpkg.com/@sentry/core@10.56.0/build/esm/feedback.js "associated_event_id: associatedEventId"',
|
|
139
|
+
async run(c) {
|
|
140
|
+
const sdk = await sdkOf('sentry-browser-postiz');
|
|
141
|
+
const client = new sdk.BrowserClient({ dsn: c.env.SENTRY_DSN, integrations: [], stackParser: sdk.defaultStackParser,
|
|
142
|
+
transport: (options: any) => sdk.createTransport(options, async (request: { body: string | Uint8Array }) => {
|
|
143
|
+
const answer = await c.fetch(options.url, { method: 'POST', body: request.body as BodyInit });
|
|
144
|
+
return { statusCode: answer.status, headers: {} };
|
|
145
|
+
}) });
|
|
146
|
+
let id = '';
|
|
147
|
+
sdk.withScope((scope: any) => {
|
|
148
|
+
scope.setClient(client); scope.setUser({ id: 'feedback-author' });
|
|
149
|
+
id = sdk.captureFeedback({ message: 'The scheduling button is hard to find', name: 'Priya', email: 'priya@harbourline.test', associatedEventId: '00000000000000000000000000000002' }, {}, scope);
|
|
150
|
+
});
|
|
151
|
+
const drained = await client.flush(10_000);
|
|
152
|
+
const event = await read(c, id, await token(c));
|
|
153
|
+
await client.close(10_000);
|
|
154
|
+
const feedback = event.contexts?.feedback;
|
|
155
|
+
return drained && event.type === 'feedback' && event.user?.id === 'feedback-author' && feedback?.name === 'Priya' && feedback?.contact_email === 'priya@harbourline.test' && feedback?.associated_event_id === '00000000000000000000000000000002' ? [] : ['feedback attribution or associated event was lost'];
|
|
156
|
+
},
|
|
157
|
+
});
|