@lokalise/playwright-reporters 1.8.0 → 1.9.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.
@@ -0,0 +1,296 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defineDatadogReporterConfig = void 0;
4
+ /* eslint-disable max-statements */
5
+ const faker_1 = require("@faker-js/faker");
6
+ const datadog_1 = require("./datadog");
7
+ const transform_1 = require("./transform");
8
+ /**
9
+ * Extracts team name from test annotations
10
+ */
11
+ const getTeamName = (test) => {
12
+ const ownerAnnotation = test.annotations?.find((annotation) => annotation.type === 'owner');
13
+ return ownerAnnotation?.description?.split(',')[0];
14
+ };
15
+ /**
16
+ * Playwright reporter that sends test execution metrics to Datadog
17
+ */
18
+ // eslint-disable-next-line import/no-default-export
19
+ class DatadogReporter {
20
+ options;
21
+ datadog;
22
+ testRunId;
23
+ // Counters
24
+ runStartTime;
25
+ runEndTime;
26
+ totalTests;
27
+ passedTests;
28
+ failedTests;
29
+ flakyTests;
30
+ skippedTests;
31
+ timedOutTests;
32
+ browserCreationMs;
33
+ retryTimeMs;
34
+ navigationMs;
35
+ workerCount;
36
+ testDurations;
37
+ // Per-test tracking
38
+ testMetrics;
39
+ projectNames;
40
+ currentTestNavigationMs;
41
+ constructor(options) {
42
+ this.options = options;
43
+ this.testRunId = faker_1.faker.string.uuid();
44
+ this.datadog = new datadog_1.Datadog({
45
+ apiKey: options.apiKey,
46
+ site: options.site,
47
+ debug: options.debug,
48
+ });
49
+ // Initialize counters
50
+ this.runStartTime = Date.now();
51
+ this.runEndTime = Date.now();
52
+ this.totalTests = 0;
53
+ this.passedTests = 0;
54
+ this.failedTests = 0;
55
+ this.flakyTests = 0;
56
+ this.skippedTests = 0;
57
+ this.timedOutTests = 0;
58
+ this.browserCreationMs = 0;
59
+ this.retryTimeMs = 0;
60
+ this.navigationMs = 0;
61
+ this.workerCount = 0;
62
+ this.testDurations = [];
63
+ this.testMetrics = [];
64
+ this.projectNames = [];
65
+ this.currentTestNavigationMs = new Map();
66
+ }
67
+ async onBegin(config, suite) {
68
+ try {
69
+ this.runStartTime = Date.now();
70
+ this.totalTests = suite.allTests().length;
71
+ this.projectNames = suite.suites.map((testSuite) => testSuite.title);
72
+ this.workerCount = config.workers;
73
+ if (!this.options.dryRun) {
74
+ await this.datadog.setNativeRequestContext();
75
+ }
76
+ if (this.options.debug) {
77
+ // eslint-disable-next-line no-console
78
+ console.log(`[Datadog Reporter] Starting test run ${this.testRunId} with ${this.totalTests} tests (${this.workerCount} workers)`);
79
+ }
80
+ }
81
+ catch (error) {
82
+ // eslint-disable-next-line no-console
83
+ console.error("[Datadog Reporter] Could not perform 'onBegin' step", error);
84
+ }
85
+ }
86
+ onStepEnd(test, result, step) {
87
+ try {
88
+ const testKey = `${test.id}-${result.retry}`;
89
+ const stepTitle = step.title.toLowerCase();
90
+ // Track browser/context/page creation time
91
+ // Captures: "Launch browser", "Create context", "Create page"
92
+ if (stepTitle.includes('launch browser') ||
93
+ stepTitle.includes('create context') ||
94
+ stepTitle.includes('create page') ||
95
+ stepTitle.includes('browsercontext.newpage') ||
96
+ stepTitle.includes('browser.newcontext')) {
97
+ this.browserCreationMs += step.duration;
98
+ }
99
+ // Track page navigation time
100
+ // Captures: "Navigate to ..." (Playwright's actual step title for goto)
101
+ if (stepTitle.includes('navigate to') ||
102
+ stepTitle.includes('page.goto') ||
103
+ stepTitle.includes('page.reload') ||
104
+ stepTitle.includes('frame.goto')) {
105
+ this.navigationMs += step.duration;
106
+ const currentNav = this.currentTestNavigationMs.get(testKey) || 0;
107
+ this.currentTestNavigationMs.set(testKey, currentNav + step.duration);
108
+ }
109
+ }
110
+ catch (error) {
111
+ // eslint-disable-next-line no-console
112
+ console.error("[Datadog Reporter] Could not perform 'onStepEnd' step", error);
113
+ }
114
+ }
115
+ onTestEnd(test, result) {
116
+ try {
117
+ const { status, retry: currentRetryCount, duration } = result;
118
+ const { retries: maxRetriesCount } = test;
119
+ const projectName = test.parent?.project()?.name;
120
+ const testKey = `${test.id}-${currentRetryCount}`;
121
+ const testNavigationMs = this.currentTestNavigationMs.get(testKey) || 0;
122
+ // Track test duration for average calculation (only on final attempt)
123
+ if (currentRetryCount === 0 || currentRetryCount === maxRetriesCount) {
124
+ this.testDurations.push(duration);
125
+ }
126
+ // Track test outcome
127
+ if (status === 'skipped') {
128
+ this.skippedTests++;
129
+ }
130
+ else if (status === 'timedOut') {
131
+ // Track timeout separately
132
+ if (currentRetryCount === maxRetriesCount) {
133
+ this.timedOutTests++;
134
+ this.failedTests++;
135
+ this.testMetrics.push({
136
+ name: test.title,
137
+ testId: test.id,
138
+ durationMs: duration,
139
+ retryCount: currentRetryCount,
140
+ isFlaky: false,
141
+ isTimedOut: true,
142
+ team: getTeamName(test),
143
+ project: projectName,
144
+ navigationMs: testNavigationMs,
145
+ });
146
+ }
147
+ if (currentRetryCount > 0) {
148
+ this.retryTimeMs += duration;
149
+ }
150
+ }
151
+ else if (['failed', 'interrupted'].includes(status)) {
152
+ // Only count as failed if all retries exhausted
153
+ if (currentRetryCount === maxRetriesCount) {
154
+ this.failedTests++;
155
+ // Track failed test metrics
156
+ this.testMetrics.push({
157
+ name: test.title,
158
+ testId: test.id,
159
+ durationMs: duration,
160
+ retryCount: currentRetryCount,
161
+ isFlaky: false,
162
+ isTimedOut: false,
163
+ team: getTeamName(test),
164
+ project: projectName,
165
+ navigationMs: testNavigationMs,
166
+ });
167
+ }
168
+ // Track retry time
169
+ if (currentRetryCount > 0) {
170
+ this.retryTimeMs += duration;
171
+ }
172
+ }
173
+ else if (status === 'passed') {
174
+ if (currentRetryCount > 0) {
175
+ // Flaky test - passed after retry
176
+ this.flakyTests++;
177
+ this.passedTests++;
178
+ this.retryTimeMs += duration;
179
+ // Track flaky test metrics
180
+ this.testMetrics.push({
181
+ name: test.title,
182
+ testId: test.id,
183
+ durationMs: duration,
184
+ retryCount: currentRetryCount,
185
+ isFlaky: true,
186
+ isTimedOut: false,
187
+ team: getTeamName(test),
188
+ project: projectName,
189
+ navigationMs: testNavigationMs,
190
+ });
191
+ }
192
+ else {
193
+ // Clean pass
194
+ this.passedTests++;
195
+ }
196
+ }
197
+ }
198
+ catch (error) {
199
+ // eslint-disable-next-line no-console
200
+ console.error("[Datadog Reporter] Could not perform 'onTestEnd' step", error);
201
+ }
202
+ }
203
+ async onEnd(result) {
204
+ try {
205
+ this.runEndTime = Date.now();
206
+ // Calculate average test duration
207
+ const avgTestDurationMs = this.testDurations.length > 0
208
+ ? Math.round(this.testDurations.reduce((a, b) => a + b, 0) / this.testDurations.length)
209
+ : 0;
210
+ // Build run metrics
211
+ const runMetrics = {
212
+ testRunId: this.testRunId,
213
+ timestamp: Math.floor(this.runStartTime / 1000), // Datadog expects seconds
214
+ totalTests: this.totalTests,
215
+ passedTests: this.passedTests,
216
+ failedTests: this.failedTests,
217
+ flakyTests: this.flakyTests,
218
+ skippedTests: this.skippedTests,
219
+ timedOutTests: this.timedOutTests,
220
+ durationMs: this.runEndTime - this.runStartTime,
221
+ browserCreationMs: this.browserCreationMs,
222
+ retryTimeMs: this.retryTimeMs,
223
+ navigationMs: this.navigationMs,
224
+ allPassed: this.failedTests === 0 && result.status === 'passed',
225
+ passRate: (0, transform_1.calculatePassRate)(this.passedTests, this.totalTests - this.skippedTests),
226
+ flakyRate: (0, transform_1.calculateFlakyRate)(this.flakyTests, this.totalTests - this.skippedTests),
227
+ workerCount: this.workerCount,
228
+ avgTestDurationMs,
229
+ };
230
+ // Build base tags
231
+ const baseTags = {
232
+ branch: this.options.ci?.branchName,
233
+ project: this.projectNames.length > 0 ? this.projectNames : undefined,
234
+ prenvId: this.options.prenvId,
235
+ triggeredBy: this.options.ci?.triggeredBy,
236
+ env: this.options.env,
237
+ service: this.options.service ?? 'playwright-tests',
238
+ product: this.options.product,
239
+ };
240
+ // Build metrics payload
241
+ const payload = (0, transform_1.buildMetricsPayload)(runMetrics, this.testMetrics, baseTags);
242
+ if (this.options.dryRun || this.options.debug) {
243
+ // eslint-disable-next-line no-console
244
+ console.log('\n[Datadog Reporter] Test Run Summary:');
245
+ // eslint-disable-next-line no-console
246
+ console.log(` Total: ${runMetrics.totalTests}`);
247
+ // eslint-disable-next-line no-console
248
+ console.log(` Passed: ${runMetrics.passedTests}`);
249
+ // eslint-disable-next-line no-console
250
+ console.log(` Failed: ${runMetrics.failedTests}`);
251
+ // eslint-disable-next-line no-console
252
+ console.log(` Flaky: ${runMetrics.flakyTests}`);
253
+ // eslint-disable-next-line no-console
254
+ console.log(` Skipped: ${runMetrics.skippedTests}`);
255
+ // eslint-disable-next-line no-console
256
+ console.log(` Timed Out: ${runMetrics.timedOutTests}`);
257
+ // eslint-disable-next-line no-console
258
+ console.log(` Pass Rate: ${runMetrics.passRate}%`);
259
+ // eslint-disable-next-line no-console
260
+ console.log(` Flaky Rate: ${runMetrics.flakyRate}%`);
261
+ // eslint-disable-next-line no-console
262
+ console.log(` Uptime: ${runMetrics.allPassed ? 1 : 0}`);
263
+ // eslint-disable-next-line no-console
264
+ console.log(` Duration: ${runMetrics.durationMs}ms`);
265
+ // eslint-disable-next-line no-console
266
+ console.log(` Avg Test Duration: ${runMetrics.avgTestDurationMs}ms`);
267
+ // eslint-disable-next-line no-console
268
+ console.log(` Browser Creation: ${runMetrics.browserCreationMs}ms`);
269
+ // eslint-disable-next-line no-console
270
+ console.log(` Navigation Time: ${runMetrics.navigationMs}ms`);
271
+ // eslint-disable-next-line no-console
272
+ console.log(` Workers: ${runMetrics.workerCount}`);
273
+ // eslint-disable-next-line no-console
274
+ console.log(` Metrics to send: ${payload.series.length}`);
275
+ }
276
+ if (!this.options.dryRun) {
277
+ await this.datadog.submitMetrics(payload);
278
+ await this.datadog.dispose();
279
+ }
280
+ if (this.options.debug) {
281
+ // eslint-disable-next-line no-console
282
+ console.log('[Datadog Reporter] Finished');
283
+ }
284
+ }
285
+ catch (error) {
286
+ // eslint-disable-next-line no-console
287
+ console.error("[Datadog Reporter] Could not perform 'onEnd' step", error);
288
+ }
289
+ }
290
+ }
291
+ exports.default = DatadogReporter;
292
+ /**
293
+ * Helper function for type-safe configuration
294
+ */
295
+ const defineDatadogReporterConfig = (options) => options;
296
+ exports.defineDatadogReporterConfig = defineDatadogReporterConfig;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const test_1 = require("@playwright/test");
4
+ /**
5
+ * Browser tests to generate browser creation time and navigation metrics.
6
+ * These tests actually launch a browser and navigate to pages.
7
+ */
8
+ test_1.test.describe('Browser Tests - Navigation Metrics', () => {
9
+ (0, test_1.test)('navigate to example.com', async ({ page }) => {
10
+ await page.goto('https://example.com');
11
+ await (0, test_1.expect)(page.locator('h1')).toContainText('Example Domain');
12
+ });
13
+ (0, test_1.test)('navigate to httpbin - GET request', async ({ page }) => {
14
+ await page.goto('https://httpbin.org/get');
15
+ await (0, test_1.expect)(page.locator('body')).toContainText('origin');
16
+ });
17
+ (0, test_1.test)('navigate and interact with elements', async ({ page }) => {
18
+ await page.goto('https://example.com');
19
+ const title = await page.title();
20
+ (0, test_1.expect)(title).toContain('Example');
21
+ await (0, test_1.expect)(page.locator('a')).toBeVisible();
22
+ });
23
+ (0, test_1.test)('multiple page navigations', async ({ page }) => {
24
+ await page.goto('https://example.com');
25
+ await (0, test_1.expect)(page.locator('h1')).toBeVisible();
26
+ await page.goto('https://httpbin.org/html');
27
+ await (0, test_1.expect)(page.locator('h1')).toBeVisible();
28
+ });
29
+ (0, test_1.test)('page with delays', async ({ page }) => {
30
+ await page.goto('https://httpbin.org/delay/1');
31
+ await (0, test_1.expect)(page.locator('body')).toContainText('origin');
32
+ });
33
+ });
34
+ test_1.test.describe('Browser Tests - Performance', () => {
35
+ (0, test_1.test)('measure page load', async ({ page }) => {
36
+ const startTime = Date.now();
37
+ await page.goto('https://example.com');
38
+ const loadTime = Date.now() - startTime;
39
+ (0, test_1.expect)(loadTime).toBeLessThan(10000); // Should load within 10s
40
+ await (0, test_1.expect)(page.locator('h1')).toBeVisible();
41
+ });
42
+ (0, test_1.test)('check page content', async ({ page }) => {
43
+ await page.goto('https://example.com');
44
+ // Check multiple elements
45
+ await (0, test_1.expect)(page.locator('h1')).toBeVisible();
46
+ await (0, test_1.expect)(page.locator('p').first()).toContainText('This domain is for use in');
47
+ await (0, test_1.expect)(page.locator('a').first()).toBeVisible();
48
+ });
49
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const test_1 = require("@playwright/test");
4
+ /**
5
+ * Sample tests to verify Datadog reporter metrics collection.
6
+ *
7
+ * This file contains:
8
+ * - Passing tests (to verify pass_rate, uptime)
9
+ * - A flaky test (to verify flaky detection)
10
+ * - A failing test (to verify failed_count)
11
+ * - A skipped test (to verify skipped_count)
12
+ */
13
+ // Track retry attempts for flaky test simulation (used internally by test)
14
+ test_1.test.describe('Datadog Reporter Test Suite', () => {
15
+ (0, test_1.test)('should pass - basic assertion', async () => {
16
+ (0, test_1.expect)(1 + 1).toBe(2);
17
+ });
18
+ (0, test_1.test)('should pass - string comparison', async () => {
19
+ (0, test_1.expect)('hello').toContain('ell');
20
+ });
21
+ (0, test_1.test)('should pass - array check', async () => {
22
+ const arr = [1, 2, 3];
23
+ (0, test_1.expect)(arr).toHaveLength(3);
24
+ });
25
+ (0, test_1.test)('should pass - object matching', async () => {
26
+ const obj = { name: 'test', value: 42 };
27
+ (0, test_1.expect)(obj).toMatchObject({ name: 'test' });
28
+ });
29
+ (0, test_1.test)('should pass - async operation', async () => {
30
+ const result = await Promise.resolve('success');
31
+ (0, test_1.expect)(result).toBe('success');
32
+ });
33
+ // This test is flaky - fails on first attempt, passes on retry
34
+ // eslint-disable-next-line no-empty-pattern
35
+ (0, test_1.test)('flaky test - passes on retry', async ({}, testInfo) => {
36
+ // Fail on first attempt (retry 0), pass on subsequent attempts
37
+ if (testInfo.retry === 0) {
38
+ (0, test_1.expect)(true).toBe(false); // Will fail
39
+ }
40
+ (0, test_1.expect)(true).toBe(true); // Will pass on retry
41
+ });
42
+ // Uncomment to test failed test tracking:
43
+ // test('should fail - intentional failure', async () => {
44
+ // expect(1).toBe(2)
45
+ // })
46
+ test_1.test.skip('skipped test - should be counted', async () => {
47
+ (0, test_1.expect)(true).toBe(true);
48
+ });
49
+ });
50
+ test_1.test.describe('Additional Passing Tests', () => {
51
+ (0, test_1.test)('math operations', async () => {
52
+ (0, test_1.expect)(10 * 5).toBe(50);
53
+ (0, test_1.expect)(100 / 4).toBe(25);
54
+ });
55
+ (0, test_1.test)('boolean logic', async () => {
56
+ (0, test_1.expect)(true && true).toBe(true);
57
+ (0, test_1.expect)(false || true).toBe(true);
58
+ });
59
+ (0, test_1.test)('type checking', async () => {
60
+ (0, test_1.expect)(typeof 'string').toBe('string');
61
+ (0, test_1.expect)(typeof 123).toBe('number');
62
+ });
63
+ });
64
+ // Team ownership annotation example
65
+ // eslint-disable-next-line no-empty-pattern
66
+ (0, test_1.test)('test with team owner', async ({}, testInfo) => {
67
+ testInfo.annotations.push({ type: 'owner', description: 'QA-Platform' });
68
+ (0, test_1.expect)(true).toBe(true);
69
+ });
@@ -0,0 +1,26 @@
1
+ import { type DatadogMetricsPayload, type MetricSeries, type MetricTags, type TestMetrics, type TestRunMetrics } from './report.types';
2
+ /**
3
+ * Converts MetricTags to Datadog tag format (key:value strings)
4
+ */
5
+ export declare const formatTags: (tags: MetricTags) => string[];
6
+ /**
7
+ * Transforms test run metrics into Datadog metric series
8
+ */
9
+ export declare const transformTestRunMetrics: (runMetrics: TestRunMetrics, baseTags: MetricTags) => MetricSeries[];
10
+ /**
11
+ * Transforms individual test metrics into Datadog metric series
12
+ * Used for tracking per-test duration and flakiness
13
+ */
14
+ export declare const transformTestMetrics: (testMetrics: TestMetrics, timestamp: number, baseTags: MetricTags) => MetricSeries[];
15
+ /**
16
+ * Builds the complete Datadog metrics payload from test run data
17
+ */
18
+ export declare const buildMetricsPayload: (runMetrics: TestRunMetrics, testMetricsList: TestMetrics[], baseTags: MetricTags) => DatadogMetricsPayload;
19
+ /**
20
+ * Calculates pass rate as a percentage
21
+ */
22
+ export declare const calculatePassRate: (passed: number, total: number) => number;
23
+ /**
24
+ * Calculates flaky rate as a percentage
25
+ */
26
+ export declare const calculateFlakyRate: (flaky: number, total: number) => number;
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateFlakyRate = exports.calculatePassRate = exports.buildMetricsPayload = exports.transformTestMetrics = exports.transformTestRunMetrics = exports.formatTags = void 0;
4
+ /* eslint-disable max-statements, max-params */
5
+ const report_types_1 = require("./report.types");
6
+ /**
7
+ * Metric name prefix for all Playwright metrics
8
+ */
9
+ const METRIC_PREFIX = 'playwright';
10
+ /**
11
+ * Converts MetricTags to Datadog tag format (key:value strings)
12
+ */
13
+ const formatTags = (tags) => {
14
+ const formattedTags = [];
15
+ if (tags.branch) {
16
+ formattedTags.push(`branch:${tags.branch}`);
17
+ }
18
+ if (tags.project && tags.project.length > 0) {
19
+ tags.project.forEach((p) => formattedTags.push(`project:${p}`));
20
+ }
21
+ if (tags.prenvId) {
22
+ formattedTags.push(`prenv_id:${tags.prenvId}`);
23
+ }
24
+ if (tags.triggeredBy) {
25
+ formattedTags.push(`triggered_by:${tags.triggeredBy}`);
26
+ }
27
+ if (tags.env) {
28
+ formattedTags.push(`env:${tags.env}`);
29
+ }
30
+ if (tags.service) {
31
+ formattedTags.push(`service:${tags.service}`);
32
+ }
33
+ if (tags.product) {
34
+ formattedTags.push(`product:${tags.product}`);
35
+ }
36
+ return formattedTags;
37
+ };
38
+ exports.formatTags = formatTags;
39
+ /**
40
+ * Creates a metric series for a gauge value
41
+ */
42
+ const createGauge = (name, value, timestamp, tags, unit) => ({
43
+ metric: `${METRIC_PREFIX}.${name}`,
44
+ type: report_types_1.MetricTypes.GAUGE,
45
+ points: [{ timestamp, value }],
46
+ tags,
47
+ ...(unit && { unit }),
48
+ });
49
+ /**
50
+ * Creates a metric series for a count value
51
+ */
52
+ const createCount = (name, value, timestamp, tags) => ({
53
+ metric: `${METRIC_PREFIX}.${name}`,
54
+ type: report_types_1.MetricTypes.COUNT,
55
+ points: [{ timestamp, value }],
56
+ tags,
57
+ });
58
+ /**
59
+ * Creates a metric series for a gauge value (used for distributions since API v2 doesn't support distribution type directly)
60
+ */
61
+ const createDistribution = (name, value, timestamp, tags, unit) => ({
62
+ metric: `${METRIC_PREFIX}.${name}`,
63
+ type: report_types_1.MetricTypes.GAUGE, // Use gauge for distribution-like metrics in v2 API
64
+ points: [{ timestamp, value }],
65
+ tags,
66
+ ...(unit && { unit }),
67
+ });
68
+ /**
69
+ * Transforms test run metrics into Datadog metric series
70
+ */
71
+ const transformTestRunMetrics = (runMetrics, baseTags) => {
72
+ const tags = (0, exports.formatTags)(baseTags);
73
+ const { timestamp } = runMetrics;
74
+ const series = [];
75
+ // Executive View Metrics
76
+ // Uptime: 1 if all tests passed, 0 otherwise
77
+ series.push(createGauge('test_run.uptime', runMetrics.allPassed ? 1 : 0, timestamp, tags));
78
+ // Pass rate as percentage (0-100)
79
+ series.push(createGauge('test_run.pass_rate', runMetrics.passRate, timestamp, tags, 'percent'));
80
+ // Engineer View Metrics
81
+ // Flaky rate as percentage (0-100)
82
+ series.push(createGauge('test_run.flaky_rate', runMetrics.flakyRate, timestamp, tags, 'percent'));
83
+ // Test counts
84
+ series.push(createGauge('test_run.test_count', runMetrics.totalTests, timestamp, tags));
85
+ series.push(createCount('test_run.failed_count', runMetrics.failedTests, timestamp, tags));
86
+ series.push(createCount('test_run.flaky_count', runMetrics.flakyTests, timestamp, tags));
87
+ series.push(createCount('test_run.skipped_count', runMetrics.skippedTests, timestamp, tags));
88
+ series.push(createCount('test_run.passed_count', runMetrics.passedTests, timestamp, tags));
89
+ // Duration metrics
90
+ series.push(createDistribution('test_run.duration_ms', runMetrics.durationMs, timestamp, tags, 'millisecond'));
91
+ // Average test duration
92
+ if (runMetrics.avgTestDurationMs > 0) {
93
+ series.push(createDistribution('test_run.avg_test_duration_ms', runMetrics.avgTestDurationMs, timestamp, tags, 'millisecond'));
94
+ }
95
+ // Browser creation time
96
+ if (runMetrics.browserCreationMs > 0) {
97
+ series.push(createDistribution('test_run.browser_creation_ms', runMetrics.browserCreationMs, timestamp, tags, 'millisecond'));
98
+ }
99
+ // Navigation time
100
+ if (runMetrics.navigationMs > 0) {
101
+ series.push(createDistribution('test_run.navigation_ms', runMetrics.navigationMs, timestamp, tags, 'millisecond'));
102
+ }
103
+ // Retry time
104
+ if (runMetrics.retryTimeMs > 0) {
105
+ series.push(createDistribution('test_run.retry_time_ms', runMetrics.retryTimeMs, timestamp, tags, 'millisecond'));
106
+ }
107
+ // Worker count
108
+ if (runMetrics.workerCount > 0) {
109
+ series.push(createGauge('test_run.worker_count', runMetrics.workerCount, timestamp, tags));
110
+ }
111
+ // Timeout count
112
+ if (runMetrics.timedOutTests > 0) {
113
+ series.push(createCount('test_run.timeout_count', runMetrics.timedOutTests, timestamp, tags));
114
+ }
115
+ return series;
116
+ };
117
+ exports.transformTestRunMetrics = transformTestRunMetrics;
118
+ /**
119
+ * Transforms individual test metrics into Datadog metric series
120
+ * Used for tracking per-test duration and flakiness
121
+ */
122
+ const transformTestMetrics = (testMetrics, timestamp, baseTags) => {
123
+ const series = [];
124
+ // Build tags with test-specific info
125
+ const testTags = (0, exports.formatTags)(baseTags);
126
+ testTags.push(`test_name:${testMetrics.name.replace(/[,|]/g, '_').substring(0, 200)}`);
127
+ if (testMetrics.team) {
128
+ testTags.push(`team:${testMetrics.team}`);
129
+ }
130
+ if (testMetrics.project) {
131
+ testTags.push(`test_project:${testMetrics.project}`);
132
+ }
133
+ // Test duration
134
+ series.push(createDistribution('test.duration_ms', testMetrics.durationMs, timestamp, testTags, 'millisecond'));
135
+ // Track navigation time per test
136
+ if (testMetrics.navigationMs > 0) {
137
+ series.push(createDistribution('test.navigation_ms', testMetrics.navigationMs, timestamp, testTags, 'millisecond'));
138
+ }
139
+ // Track retries
140
+ if (testMetrics.retryCount > 0) {
141
+ series.push(createCount('test.retry_count', testMetrics.retryCount, timestamp, testTags));
142
+ }
143
+ // Track flaky tests
144
+ if (testMetrics.isFlaky) {
145
+ series.push(createCount('test.flaky', 1, timestamp, testTags));
146
+ }
147
+ // Track timed out tests
148
+ if (testMetrics.isTimedOut) {
149
+ series.push(createCount('test.timeout', 1, timestamp, testTags));
150
+ }
151
+ return series;
152
+ };
153
+ exports.transformTestMetrics = transformTestMetrics;
154
+ /**
155
+ * Builds the complete Datadog metrics payload from test run data
156
+ */
157
+ const buildMetricsPayload = (runMetrics, testMetricsList, baseTags) => {
158
+ const series = [];
159
+ // Add test run level metrics
160
+ series.push(...(0, exports.transformTestRunMetrics)(runMetrics, baseTags));
161
+ // Add individual test metrics (for flaky tests and duration tracking)
162
+ for (const testMetrics of testMetricsList) {
163
+ series.push(...(0, exports.transformTestMetrics)(testMetrics, runMetrics.timestamp, baseTags));
164
+ }
165
+ return { series };
166
+ };
167
+ exports.buildMetricsPayload = buildMetricsPayload;
168
+ /**
169
+ * Calculates pass rate as a percentage
170
+ */
171
+ const calculatePassRate = (passed, total) => {
172
+ if (total === 0)
173
+ return 100;
174
+ return Math.round((passed / total) * 10000) / 100; // Round to 2 decimal places
175
+ };
176
+ exports.calculatePassRate = calculatePassRate;
177
+ /**
178
+ * Calculates flaky rate as a percentage
179
+ */
180
+ const calculateFlakyRate = (flaky, total) => {
181
+ if (total === 0)
182
+ return 0;
183
+ return Math.round((flaky / total) * 10000) / 100; // Round to 2 decimal places
184
+ };
185
+ exports.calculateFlakyRate = calculateFlakyRate;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import AnalyticsReporter, { defineAnalyticsReporterConfig } from './analytics/reporter';
2
+ import DatadogReporter, { defineDatadogReporterConfig } from './datadog/reporter';
2
3
  import PermissionsReporter, { definePermissionsReporterConfig } from './permissions/reporter';
3
4
  import RetryReporter, { defineRetryReporterConfig } from './retry/reporter';
4
5
  import StepDurationReporter, { defineStepDurationReporterConfig } from './stepDuration/reporter';
5
6
  import TimelineReporter, { defineTimelineReporterConfig } from './timeline/reporter';
6
7
  import TranslationReporter, { defineTranslationReporterConfig } from './translation/reporter';
7
- export { AnalyticsReporter, defineAnalyticsReporterConfig, RetryReporter, defineRetryReporterConfig, TimelineReporter, defineTimelineReporterConfig, StepDurationReporter, defineStepDurationReporterConfig, PermissionsReporter, definePermissionsReporterConfig, TranslationReporter, defineTranslationReporterConfig, };
8
+ export { AnalyticsReporter, defineAnalyticsReporterConfig, DatadogReporter, defineDatadogReporterConfig, RetryReporter, defineRetryReporterConfig, TimelineReporter, defineTimelineReporterConfig, StepDurationReporter, defineStepDurationReporterConfig, PermissionsReporter, definePermissionsReporterConfig, TranslationReporter, defineTranslationReporterConfig, };