@data_engine_labs/data_engine_sdk 0.1.0

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/index.js ADDED
@@ -0,0 +1,322 @@
1
+ // src/client.ts
2
+ var DEFAULT_TIMEOUT_MS = 15e3;
3
+ var DEFAULT_MAX_RETRIES = 3;
4
+ var RETRY_DELAYS_MS = [200, 600, 1800];
5
+ var DataEngineHttpError = class extends Error {
6
+ status;
7
+ errors;
8
+ constructor(status, message, errors) {
9
+ super(message);
10
+ this.name = "DataEngineHttpError";
11
+ this.status = status;
12
+ this.errors = errors;
13
+ }
14
+ };
15
+ function sleep(ms) {
16
+ return new Promise((r) => setTimeout(r, ms));
17
+ }
18
+ function isRetriable(status) {
19
+ return status >= 500 && status < 600;
20
+ }
21
+ function createClient(options) {
22
+ const {
23
+ apiKey,
24
+ baseUrl,
25
+ fetch: fetchImpl = fetch,
26
+ timeoutMs = DEFAULT_TIMEOUT_MS,
27
+ maxRetries = DEFAULT_MAX_RETRIES
28
+ } = options;
29
+ const trimmedBase = baseUrl.replace(/\/+$/, "");
30
+ async function post(payload) {
31
+ const controller = new AbortController();
32
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
33
+ try {
34
+ return await fetchImpl(`${trimmedBase}/api/v1/test-runs`, {
35
+ method: "POST",
36
+ headers: {
37
+ "content-type": "application/json",
38
+ authorization: `Bearer ${apiKey}`
39
+ },
40
+ body: JSON.stringify(payload),
41
+ signal: controller.signal
42
+ });
43
+ } finally {
44
+ clearTimeout(timer);
45
+ }
46
+ }
47
+ async function submitRun(payload) {
48
+ let lastError;
49
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
50
+ try {
51
+ const res = await post(payload);
52
+ if (res.ok) {
53
+ return await res.json();
54
+ }
55
+ const bodyText = await res.text().catch(() => "");
56
+ const parsed = bodyText ? safeJson(bodyText) : null;
57
+ const message = (parsed && typeof parsed === "object" && "message" in parsed ? String(parsed.message) : void 0) ?? `HTTP ${res.status}`;
58
+ const errors = parsed && typeof parsed === "object" && "errors" in parsed ? parsed.errors : void 0;
59
+ const err = new DataEngineHttpError(res.status, message, errors);
60
+ if (!isRetriable(res.status) || attempt === maxRetries) throw err;
61
+ lastError = err;
62
+ } catch (e) {
63
+ if (e instanceof DataEngineHttpError && !isRetriable(e.status)) throw e;
64
+ lastError = e;
65
+ if (attempt === maxRetries) throw e;
66
+ }
67
+ await sleep(RETRY_DELAYS_MS[attempt] ?? RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1]);
68
+ }
69
+ throw lastError instanceof Error ? lastError : new Error("submitRun failed");
70
+ }
71
+ return { submitRun };
72
+ }
73
+ function safeJson(text) {
74
+ try {
75
+ return JSON.parse(text);
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ // src/config.ts
82
+ function pickString(...values) {
83
+ for (const v of values) {
84
+ if (typeof v === "string" && v.length > 0) return v;
85
+ }
86
+ return void 0;
87
+ }
88
+ function parseBool(v) {
89
+ return v === "true" || v === "1" || v === "yes";
90
+ }
91
+ function loadConfig(overrides = {}, env = process.env) {
92
+ const apiKey = pickString(overrides.apiKey, env.DATA_ENGINE_API_KEY);
93
+ const baseUrl = pickString(overrides.baseUrl, env.DATA_ENGINE_BASE_URL);
94
+ const environment = pickString(overrides.environment, env.DATA_ENGINE_ENVIRONMENT);
95
+ const missing = [];
96
+ if (!apiKey) missing.push("DATA_ENGINE_API_KEY");
97
+ if (!baseUrl) missing.push("DATA_ENGINE_BASE_URL");
98
+ if (!environment) missing.push("DATA_ENGINE_ENVIRONMENT");
99
+ const disabled = overrides.disabled ?? parseBool(env.DATA_ENGINE_DISABLE);
100
+ const strict = overrides.strict ?? parseBool(env.DATA_ENGINE_STRICT);
101
+ if (missing.length > 0) return { ok: false, missing };
102
+ return {
103
+ ok: true,
104
+ missing: [],
105
+ config: {
106
+ apiKey,
107
+ baseUrl: baseUrl.replace(/\/+$/, ""),
108
+ environment,
109
+ branch: pickString(overrides.branch, env.DATA_ENGINE_BRANCH),
110
+ commitSha: pickString(overrides.commitSha, env.DATA_ENGINE_COMMIT_SHA),
111
+ triggeredBy: pickString(overrides.triggeredBy, env.DATA_ENGINE_TRIGGERED_BY),
112
+ pipelineRunUrl: pickString(
113
+ overrides.pipelineRunUrl,
114
+ env.DATA_ENGINE_PIPELINE_RUN_URL
115
+ ),
116
+ strict,
117
+ disabled
118
+ }
119
+ };
120
+ }
121
+
122
+ // src/pipeline/providers/github.ts
123
+ function detectGitHub(env) {
124
+ if (env.GITHUB_ACTIONS !== "true") return null;
125
+ const server = env.GITHUB_SERVER_URL;
126
+ const repo = env.GITHUB_REPOSITORY;
127
+ const runId = env.GITHUB_RUN_ID;
128
+ const pipelineRunUrl = server && repo && runId ? `${server}/${repo}/actions/runs/${runId}` : void 0;
129
+ return {
130
+ provider: "github",
131
+ branch: env.GITHUB_REF_NAME,
132
+ commitSha: env.GITHUB_SHA,
133
+ triggeredBy: env.GITHUB_ACTOR,
134
+ pipelineRunUrl
135
+ };
136
+ }
137
+
138
+ // src/pipeline/providers/azure.ts
139
+ function detectAzure(env) {
140
+ if (env.TF_BUILD !== "True" && env.TF_BUILD !== "true") return null;
141
+ const collection = env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
142
+ const project = env.SYSTEM_TEAMPROJECT;
143
+ const buildId = env.BUILD_BUILDID;
144
+ const pipelineRunUrl = collection && project && buildId ? `${collection.replace(/\/+$/, "")}/${encodeURIComponent(project)}/_build/results?buildId=${buildId}` : void 0;
145
+ return {
146
+ provider: "azure",
147
+ branch: env.BUILD_SOURCEBRANCHNAME,
148
+ commitSha: env.BUILD_SOURCEVERSION,
149
+ triggeredBy: env.BUILD_REQUESTEDFOR,
150
+ pipelineRunUrl
151
+ };
152
+ }
153
+
154
+ // src/pipeline/providers/gitlab.ts
155
+ function detectGitLab(env) {
156
+ if (env.GITLAB_CI !== "true") return null;
157
+ return {
158
+ provider: "gitlab",
159
+ branch: env.CI_COMMIT_REF_NAME,
160
+ commitSha: env.CI_COMMIT_SHA,
161
+ triggeredBy: env.GITLAB_USER_LOGIN,
162
+ pipelineRunUrl: env.CI_PIPELINE_URL
163
+ };
164
+ }
165
+
166
+ // src/pipeline/providers/generic.ts
167
+ function detectGeneric(env) {
168
+ if (env.CI !== "true" && env.CI !== "1") return null;
169
+ return {
170
+ provider: "generic",
171
+ branch: env.CI_COMMIT_REF_NAME ?? env.GIT_BRANCH,
172
+ commitSha: env.CI_COMMIT_SHA ?? env.GIT_COMMIT,
173
+ triggeredBy: env.USER,
174
+ pipelineRunUrl: void 0
175
+ };
176
+ }
177
+
178
+ // src/pipeline/detect.ts
179
+ function detectPipeline(env = process.env) {
180
+ return detectGitHub(env) ?? detectAzure(env) ?? detectGitLab(env) ?? detectGeneric(env) ?? { provider: "unknown" };
181
+ }
182
+
183
+ // src/pipeline/merge.ts
184
+ function mergePipeline(detected, config) {
185
+ return {
186
+ branch: config.branch ?? detected.branch,
187
+ commitSha: config.commitSha ?? detected.commitSha,
188
+ triggeredBy: config.triggeredBy ?? detected.triggeredBy,
189
+ pipelineRunUrl: config.pipelineRunUrl ?? detected.pipelineRunUrl,
190
+ provider: detected.provider
191
+ };
192
+ }
193
+
194
+ // src/playwright/reporter.ts
195
+ import { randomUUID } from "crypto";
196
+
197
+ // src/playwright/map-result.ts
198
+ var ERROR_MESSAGE_MAX = 4e3;
199
+ function truncate(s, max) {
200
+ return s.length > max ? s.slice(0, max) : s;
201
+ }
202
+ function mapStatus(pw, retry) {
203
+ if (pw === "skipped") return "SKIPPED";
204
+ if (pw === "passed") return retry > 0 ? "FLAKY" : "PASSED";
205
+ return "FAILED";
206
+ }
207
+ function suiteOf(test) {
208
+ const parts = test.titlePath ? test.titlePath() : test.parent?.titlePath?.() ?? [];
209
+ const withoutLeaf = parts.slice(0, Math.max(0, parts.length - 1)).filter(Boolean);
210
+ if (withoutLeaf.length > 0) return withoutLeaf.join(" > ");
211
+ return test.location?.file ?? "unknown";
212
+ }
213
+ function mapPlaywrightResult(test, result) {
214
+ const status = mapStatus(result.status, result.retry);
215
+ const rawError = result.errors?.[0]?.message;
216
+ return {
217
+ name: test.title,
218
+ suite: suiteOf(test),
219
+ status,
220
+ durationMs: Math.max(0, Math.round(result.duration)),
221
+ errorMessage: rawError ? truncate(rawError, ERROR_MESSAGE_MAX) : void 0,
222
+ retries: result.retry,
223
+ tags: []
224
+ };
225
+ }
226
+
227
+ // src/playwright/reporter.ts
228
+ var DataEngineReporter = class {
229
+ enabled = false;
230
+ config;
231
+ client;
232
+ pipeline;
233
+ framework;
234
+ startedAt = /* @__PURE__ */ new Date();
235
+ buffered = /* @__PURE__ */ new Map();
236
+ injectedClient;
237
+ constructor(options = {}) {
238
+ this.framework = options.framework ?? "PLAYWRIGHT";
239
+ this.injectedClient = options.client;
240
+ this.pendingOverrides = {
241
+ apiKey: options.apiKey,
242
+ baseUrl: options.baseUrl,
243
+ environment: options.environment,
244
+ branch: options.branch,
245
+ commitSha: options.commitSha,
246
+ triggeredBy: options.triggeredBy,
247
+ pipelineRunUrl: options.pipelineRunUrl,
248
+ strict: options.strict,
249
+ disabled: options.disabled
250
+ };
251
+ }
252
+ pendingOverrides;
253
+ printsToStdio() {
254
+ return false;
255
+ }
256
+ onBegin() {
257
+ this.startedAt = /* @__PURE__ */ new Date();
258
+ const loaded = loadConfig(this.pendingOverrides);
259
+ if (!loaded.ok) {
260
+ this.warnOnce(
261
+ `[data-engine] reporter disabled \u2014 missing: ${loaded.missing.join(", ")}`
262
+ );
263
+ this.enabled = false;
264
+ return;
265
+ }
266
+ this.config = loaded.config;
267
+ if (this.config.disabled) {
268
+ this.warnOnce("[data-engine] reporter disabled via DATA_ENGINE_DISABLE");
269
+ this.enabled = false;
270
+ return;
271
+ }
272
+ this.client = this.injectedClient ?? createClient({ apiKey: this.config.apiKey, baseUrl: this.config.baseUrl });
273
+ this.pipeline = mergePipeline(detectPipeline(), this.config);
274
+ this.enabled = true;
275
+ }
276
+ onTestEnd(test, result) {
277
+ if (!this.enabled) return;
278
+ this.buffered.set(test.id, mapPlaywrightResult(test, result));
279
+ }
280
+ async onEnd() {
281
+ if (!this.enabled || !this.client || !this.config || !this.pipeline) return;
282
+ const results = Array.from(this.buffered.values());
283
+ if (results.length === 0) return;
284
+ const finishedAt = /* @__PURE__ */ new Date();
285
+ const payload = {
286
+ runId: randomUUID(),
287
+ framework: this.framework,
288
+ environment: this.config.environment,
289
+ branch: this.pipeline.branch ?? "unknown",
290
+ commitSha: this.pipeline.commitSha,
291
+ triggeredBy: this.pipeline.triggeredBy,
292
+ pipelineRunUrl: this.pipeline.pipelineRunUrl,
293
+ startedAt: this.startedAt.toISOString(),
294
+ finishedAt: finishedAt.toISOString(),
295
+ results
296
+ };
297
+ try {
298
+ await this.client.submitRun(payload);
299
+ } catch (error) {
300
+ const msg = error instanceof Error ? error.message : String(error);
301
+ if (this.config.strict) {
302
+ throw error;
303
+ }
304
+ console.warn(`[data-engine] submit failed: ${msg}`);
305
+ }
306
+ }
307
+ warned = false;
308
+ warnOnce(message) {
309
+ if (this.warned) return;
310
+ this.warned = true;
311
+ console.warn(message);
312
+ }
313
+ };
314
+ export {
315
+ DataEngineHttpError,
316
+ DataEngineReporter,
317
+ createClient,
318
+ detectPipeline,
319
+ loadConfig,
320
+ mergePipeline
321
+ };
322
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/config.ts","../src/pipeline/providers/github.ts","../src/pipeline/providers/azure.ts","../src/pipeline/providers/gitlab.ts","../src/pipeline/providers/generic.ts","../src/pipeline/detect.ts","../src/pipeline/merge.ts","../src/playwright/reporter.ts","../src/playwright/map-result.ts"],"sourcesContent":["import type {\n SubmitRunError,\n SubmitRunResponse,\n TestRunPayload,\n} from \"./types.js\";\n\nexport type ClientOptions = {\n apiKey: string;\n baseUrl: string;\n fetch?: typeof fetch;\n timeoutMs?: number;\n maxRetries?: number;\n};\n\nexport type Client = {\n submitRun(payload: TestRunPayload): Promise<SubmitRunResponse>;\n};\n\nconst DEFAULT_TIMEOUT_MS = 15_000;\nconst DEFAULT_MAX_RETRIES = 3;\nconst RETRY_DELAYS_MS = [200, 600, 1800];\n\nexport class DataEngineHttpError extends Error implements SubmitRunError {\n readonly status: number;\n readonly errors?: SubmitRunError[\"errors\"];\n constructor(status: number, message: string, errors?: SubmitRunError[\"errors\"]) {\n super(message);\n this.name = \"DataEngineHttpError\";\n this.status = status;\n this.errors = errors;\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\nfunction isRetriable(status: number): boolean {\n return status >= 500 && status < 600;\n}\n\nexport function createClient(options: ClientOptions): Client {\n const {\n apiKey,\n baseUrl,\n fetch: fetchImpl = fetch,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n maxRetries = DEFAULT_MAX_RETRIES,\n } = options;\n const trimmedBase = baseUrl.replace(/\\/+$/, \"\");\n\n async function post(payload: TestRunPayload): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetchImpl(`${trimmedBase}/api/v1/test-runs`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n });\n } finally {\n clearTimeout(timer);\n }\n }\n\n async function submitRun(payload: TestRunPayload): Promise<SubmitRunResponse> {\n let lastError: unknown;\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const res = await post(payload);\n if (res.ok) {\n return (await res.json()) as SubmitRunResponse;\n }\n const bodyText = await res.text().catch(() => \"\");\n const parsed = bodyText ? safeJson(bodyText) : null;\n const message =\n (parsed && typeof parsed === \"object\" && \"message\" in parsed\n ? String((parsed as { message: unknown }).message)\n : undefined) ?? `HTTP ${res.status}`;\n const errors =\n parsed && typeof parsed === \"object\" && \"errors\" in parsed\n ? ((parsed as { errors: SubmitRunError[\"errors\"] }).errors)\n : undefined;\n const err = new DataEngineHttpError(res.status, message, errors);\n if (!isRetriable(res.status) || attempt === maxRetries) throw err;\n lastError = err;\n } catch (e) {\n if (e instanceof DataEngineHttpError && !isRetriable(e.status)) throw e;\n lastError = e;\n if (attempt === maxRetries) throw e;\n }\n await sleep(RETRY_DELAYS_MS[attempt] ?? RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1]);\n }\n throw lastError instanceof Error ? lastError : new Error(\"submitRun failed\");\n }\n\n return { submitRun };\n}\n\nfunction safeJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n","import type { ReporterOverrides, SdkConfig } from \"./types.js\";\n\nexport type LoadConfigResult =\n | { ok: true; config: SdkConfig; missing: [] }\n | { ok: false; missing: string[] };\n\nfunction pickString(...values: Array<string | undefined>): string | undefined {\n for (const v of values) {\n if (typeof v === \"string\" && v.length > 0) return v;\n }\n return undefined;\n}\n\nfunction parseBool(v: string | undefined): boolean {\n return v === \"true\" || v === \"1\" || v === \"yes\";\n}\n\n// Precedence per field: overrides > env var > (detector merged elsewhere)\nexport function loadConfig(\n overrides: ReporterOverrides = {},\n env: NodeJS.ProcessEnv = process.env,\n): LoadConfigResult {\n const apiKey = pickString(overrides.apiKey, env.DATA_ENGINE_API_KEY);\n const baseUrl = pickString(overrides.baseUrl, env.DATA_ENGINE_BASE_URL);\n const environment = pickString(overrides.environment, env.DATA_ENGINE_ENVIRONMENT);\n\n const missing: string[] = [];\n if (!apiKey) missing.push(\"DATA_ENGINE_API_KEY\");\n if (!baseUrl) missing.push(\"DATA_ENGINE_BASE_URL\");\n if (!environment) missing.push(\"DATA_ENGINE_ENVIRONMENT\");\n\n const disabled = overrides.disabled ?? parseBool(env.DATA_ENGINE_DISABLE);\n const strict = overrides.strict ?? parseBool(env.DATA_ENGINE_STRICT);\n\n if (missing.length > 0) return { ok: false, missing };\n\n return {\n ok: true,\n missing: [],\n config: {\n apiKey: apiKey!,\n baseUrl: baseUrl!.replace(/\\/+$/, \"\"),\n environment: environment!,\n branch: pickString(overrides.branch, env.DATA_ENGINE_BRANCH),\n commitSha: pickString(overrides.commitSha, env.DATA_ENGINE_COMMIT_SHA),\n triggeredBy: pickString(overrides.triggeredBy, env.DATA_ENGINE_TRIGGERED_BY),\n pipelineRunUrl: pickString(\n overrides.pipelineRunUrl,\n env.DATA_ENGINE_PIPELINE_RUN_URL,\n ),\n strict,\n disabled,\n },\n };\n}\n","import type { PipelineContext } from \"../../types.js\";\n\n// GitHub Actions env reference:\n// GITHUB_ACTIONS=true, GITHUB_REF_NAME=branch, GITHUB_SHA=commit,\n// GITHUB_SERVER_URL + GITHUB_REPOSITORY + GITHUB_RUN_ID → run URL,\n// GITHUB_ACTOR=triggered by\nexport function detectGitHub(env: NodeJS.ProcessEnv): PipelineContext | null {\n if (env.GITHUB_ACTIONS !== \"true\") return null;\n const server = env.GITHUB_SERVER_URL;\n const repo = env.GITHUB_REPOSITORY;\n const runId = env.GITHUB_RUN_ID;\n const pipelineRunUrl =\n server && repo && runId ? `${server}/${repo}/actions/runs/${runId}` : undefined;\n return {\n provider: \"github\",\n branch: env.GITHUB_REF_NAME,\n commitSha: env.GITHUB_SHA,\n triggeredBy: env.GITHUB_ACTOR,\n pipelineRunUrl,\n };\n}\n","import type { PipelineContext } from \"../../types.js\";\n\n// Azure Pipelines env reference:\n// TF_BUILD=True, BUILD_SOURCEBRANCHNAME=branch, BUILD_SOURCEVERSION=commit,\n// SYSTEM_TEAMFOUNDATIONCOLLECTIONURI + SYSTEM_TEAMPROJECT + BUILD_BUILDID → run URL,\n// BUILD_REQUESTEDFOR=triggered by (display name)\nexport function detectAzure(env: NodeJS.ProcessEnv): PipelineContext | null {\n if (env.TF_BUILD !== \"True\" && env.TF_BUILD !== \"true\") return null;\n const collection = env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;\n const project = env.SYSTEM_TEAMPROJECT;\n const buildId = env.BUILD_BUILDID;\n const pipelineRunUrl =\n collection && project && buildId\n ? `${collection.replace(/\\/+$/, \"\")}/${encodeURIComponent(project)}/_build/results?buildId=${buildId}`\n : undefined;\n return {\n provider: \"azure\",\n branch: env.BUILD_SOURCEBRANCHNAME,\n commitSha: env.BUILD_SOURCEVERSION,\n triggeredBy: env.BUILD_REQUESTEDFOR,\n pipelineRunUrl,\n };\n}\n","import type { PipelineContext } from \"../../types.js\";\n\n// GitLab CI env reference:\n// GITLAB_CI=true, CI_COMMIT_REF_NAME=branch, CI_COMMIT_SHA=commit,\n// CI_PIPELINE_URL=run URL, GITLAB_USER_LOGIN=triggered by\nexport function detectGitLab(env: NodeJS.ProcessEnv): PipelineContext | null {\n if (env.GITLAB_CI !== \"true\") return null;\n return {\n provider: \"gitlab\",\n branch: env.CI_COMMIT_REF_NAME,\n commitSha: env.CI_COMMIT_SHA,\n triggeredBy: env.GITLAB_USER_LOGIN,\n pipelineRunUrl: env.CI_PIPELINE_URL,\n };\n}\n","import type { PipelineContext } from \"../../types.js\";\n\n// Generic CI fallback: only fires when CI=true and no known provider matched.\n// Reads a handful of widely-set env vars but does not shell out to git.\nexport function detectGeneric(env: NodeJS.ProcessEnv): PipelineContext | null {\n if (env.CI !== \"true\" && env.CI !== \"1\") return null;\n return {\n provider: \"generic\",\n branch: env.CI_COMMIT_REF_NAME ?? env.GIT_BRANCH,\n commitSha: env.CI_COMMIT_SHA ?? env.GIT_COMMIT,\n triggeredBy: env.USER,\n pipelineRunUrl: undefined,\n };\n}\n","import type { PipelineContext } from \"../types.js\";\nimport { detectGitHub } from \"./providers/github.js\";\nimport { detectAzure } from \"./providers/azure.js\";\nimport { detectGitLab } from \"./providers/gitlab.js\";\nimport { detectGeneric } from \"./providers/generic.js\";\n\nexport function detectPipeline(\n env: NodeJS.ProcessEnv = process.env,\n): PipelineContext {\n return (\n detectGitHub(env) ??\n detectAzure(env) ??\n detectGitLab(env) ??\n detectGeneric(env) ?? { provider: \"unknown\" }\n );\n}\n","import type { PipelineContext, SdkConfig } from \"../types.js\";\n\n// Merge detected pipeline context with SdkConfig overrides.\n// The config field wins whenever it is defined; detector fills the gaps.\nexport type MergedPipeline = {\n branch: string | undefined;\n commitSha: string | undefined;\n triggeredBy: string | undefined;\n pipelineRunUrl: string | undefined;\n provider: PipelineContext[\"provider\"];\n};\n\nexport function mergePipeline(\n detected: PipelineContext,\n config: SdkConfig,\n): MergedPipeline {\n return {\n branch: config.branch ?? detected.branch,\n commitSha: config.commitSha ?? detected.commitSha,\n triggeredBy: config.triggeredBy ?? detected.triggeredBy,\n pipelineRunUrl: config.pipelineRunUrl ?? detected.pipelineRunUrl,\n provider: detected.provider,\n };\n}\n","import { randomUUID } from \"node:crypto\";\nimport type { Reporter, TestCase, TestResult } from \"@playwright/test/reporter\";\nimport { loadConfig } from \"../config.js\";\nimport { createClient, type Client } from \"../client.js\";\nimport { detectPipeline } from \"../pipeline/detect.js\";\nimport { mergePipeline, type MergedPipeline } from \"../pipeline/merge.js\";\nimport { mapPlaywrightResult } from \"./map-result.js\";\nimport type {\n Framework,\n ReporterOverrides,\n SdkConfig,\n TestResultPayload,\n} from \"../types.js\";\n\nexport type DataEngineReporterOptions = ReporterOverrides & {\n framework?: Framework;\n /**\n * Inject a client for testing. Not intended for production use.\n * @internal\n */\n client?: Client;\n};\n\n// Custom Playwright Reporter that captures the full run + pipeline context\n// and posts it to POST /api/v1/test-runs on portal shutdown.\n//\n// Fail-open by default: any network / config error is warned but never thrown.\n// DATA_ENGINE_STRICT=true flips to fail-loud.\nexport default class DataEngineReporter implements Reporter {\n private enabled = false;\n private config?: SdkConfig;\n private client?: Client;\n private pipeline?: MergedPipeline;\n private framework: Framework;\n private startedAt = new Date();\n private buffered = new Map<string, TestResultPayload>();\n private injectedClient?: Client;\n\n constructor(options: DataEngineReporterOptions = {}) {\n this.framework = options.framework ?? \"PLAYWRIGHT\";\n this.injectedClient = options.client;\n // Stash overrides (minus the framework/client-specific keys) for onBegin.\n this.pendingOverrides = {\n apiKey: options.apiKey,\n baseUrl: options.baseUrl,\n environment: options.environment,\n branch: options.branch,\n commitSha: options.commitSha,\n triggeredBy: options.triggeredBy,\n pipelineRunUrl: options.pipelineRunUrl,\n strict: options.strict,\n disabled: options.disabled,\n };\n }\n\n private pendingOverrides: ReporterOverrides;\n\n printsToStdio(): boolean {\n return false;\n }\n\n onBegin(): void {\n this.startedAt = new Date();\n const loaded = loadConfig(this.pendingOverrides);\n if (!loaded.ok) {\n this.warnOnce(\n `[data-engine] reporter disabled — missing: ${loaded.missing.join(\", \")}`,\n );\n this.enabled = false;\n return;\n }\n this.config = loaded.config;\n if (this.config.disabled) {\n this.warnOnce(\"[data-engine] reporter disabled via DATA_ENGINE_DISABLE\");\n this.enabled = false;\n return;\n }\n this.client =\n this.injectedClient ??\n createClient({ apiKey: this.config.apiKey, baseUrl: this.config.baseUrl });\n this.pipeline = mergePipeline(detectPipeline(), this.config);\n this.enabled = true;\n }\n\n onTestEnd(test: TestCase, result: TestResult): void {\n if (!this.enabled) return;\n // Playwright fires onTestEnd once per attempt. We keep the latest by test.id\n // so retries collapse into a single result (the final attempt wins).\n this.buffered.set(test.id, mapPlaywrightResult(test, result));\n }\n\n async onEnd(): Promise<void> {\n if (!this.enabled || !this.client || !this.config || !this.pipeline) return;\n const results = Array.from(this.buffered.values());\n if (results.length === 0) return;\n\n const finishedAt = new Date();\n const payload = {\n runId: randomUUID(),\n framework: this.framework,\n environment: this.config.environment,\n branch: this.pipeline.branch ?? \"unknown\",\n commitSha: this.pipeline.commitSha,\n triggeredBy: this.pipeline.triggeredBy,\n pipelineRunUrl: this.pipeline.pipelineRunUrl,\n startedAt: this.startedAt.toISOString(),\n finishedAt: finishedAt.toISOString(),\n results,\n };\n\n try {\n await this.client.submitRun(payload);\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n if (this.config.strict) {\n throw error;\n }\n console.warn(`[data-engine] submit failed: ${msg}`);\n }\n }\n\n private warned = false;\n private warnOnce(message: string): void {\n if (this.warned) return;\n this.warned = true;\n console.warn(message);\n }\n}\n","import type { TestResultPayload, TestResultStatus } from \"../types.js\";\n\n// Minimal shape of Playwright's TestCase/TestResult that we consume.\n// We don't import from @playwright/test to keep this module test-friendly\n// and avoid a hard runtime dependency; the Reporter imports the real types.\nexport type MinimalTestCase = {\n id: string;\n title: string;\n location?: { file?: string };\n parent?: { titlePath?: () => string[] };\n titlePath?: () => string[];\n};\n\nexport type MinimalTestResult = {\n status: \"passed\" | \"failed\" | \"timedOut\" | \"skipped\" | \"interrupted\";\n duration: number;\n retry: number;\n errors: Array<{ message?: string }>;\n};\n\nconst ERROR_MESSAGE_MAX = 4000;\n\nfunction truncate(s: string, max: number): string {\n return s.length > max ? s.slice(0, max) : s;\n}\n\nfunction mapStatus(\n pw: MinimalTestResult[\"status\"],\n retry: number,\n): TestResultStatus {\n if (pw === \"skipped\") return \"SKIPPED\";\n if (pw === \"passed\") return retry > 0 ? \"FLAKY\" : \"PASSED\";\n // failed, timedOut, interrupted → FAILED\n return \"FAILED\";\n}\n\nfunction suiteOf(test: MinimalTestCase): string {\n const parts = test.titlePath ? test.titlePath() : test.parent?.titlePath?.() ?? [];\n // Playwright's titlePath returns [projectName?, file, ...describes, testTitle].\n // We drop the last entry (test title) and empty leading entries.\n const withoutLeaf = parts.slice(0, Math.max(0, parts.length - 1)).filter(Boolean);\n if (withoutLeaf.length > 0) return withoutLeaf.join(\" > \");\n return test.location?.file ?? \"unknown\";\n}\n\nexport function mapPlaywrightResult(\n test: MinimalTestCase,\n result: MinimalTestResult,\n): TestResultPayload {\n const status = mapStatus(result.status, result.retry);\n const rawError = result.errors?.[0]?.message;\n return {\n name: test.title,\n suite: suiteOf(test),\n status,\n durationMs: Math.max(0, Math.round(result.duration)),\n errorMessage: rawError ? truncate(rawError, ERROR_MESSAGE_MAX) : undefined,\n retries: result.retry,\n tags: [],\n };\n}\n"],"mappings":"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","names":[]}