@gitlab/jest-metrics-exporter 0.0.5

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/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) GitLab Inc
2
+
3
+ With regard to the GitLab Software:
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
23
+ For all third party components incorporated into the GitLab Software, those
24
+ components are licensed under the original license provided by the owner of the
25
+ applicable component.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # @gitlab/jest-metrics-exporter
2
+
3
+ A Jest reporter that collects test execution data and pushes it to the GitLab
4
+ Quality Observer service.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pnpm add -D @gitlab/jest-metrics-exporter
10
+ # or: npm install --save-dev @gitlab/jest-metrics-exporter
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ## Preconditions
16
+
17
+ For reporter to correctly detect file locations,
18
+ it requires flag [--testLocationInResults](https://jestjs.io/docs/cli#--testlocationinresults) to be set.
19
+
20
+ ### Minimal (env-only) setup
21
+
22
+ Register the reporter in `jest.config.js` and rely on environment variables
23
+ for everything else:
24
+
25
+ ```js
26
+ module.exports = {
27
+ testLocationInResults: true,
28
+ reporters: ['default', '@gitlab/jest-metrics-exporter'],
29
+ };
30
+ ```
31
+
32
+ `testLocationInResults: true` is required — without it the reporter logs an
33
+ error and skips export for the run.
34
+
35
+ ### Full setup with options and predicates
36
+
37
+ For function-typed options (`skipRecord`, `testRetried`, `customMetrics`),
38
+ import the reporter class and use the tuple form:
39
+
40
+ ```js
41
+ module.exports = {
42
+ testLocationInResults: true,
43
+ reporters: [
44
+ 'default',
45
+ ['@gitlab/jest-metrics-exporter', {
46
+ runType: 'frontend_unit',
47
+ specFilePathPrefix: 'frontend/',
48
+ skipRecord: (testCase) => testCase.fullName.includes('@flaky'),
49
+ testRetried: (testCase) => testCase.invocations > 1,
50
+ customMetrics: (testCase) => ({
51
+ pipelineType: 'merge_request_pipeline',
52
+ }),
53
+ }],
54
+ ],
55
+ };
56
+ ```
57
+
58
+ ## Configuration options
59
+
60
+ Options can be passed via the reporter-options object (second element of the
61
+ tuple in `reporters`) or via environment variables. Explicit options always
62
+ take precedence over the corresponding environment variable.
63
+
64
+ | Option | Env var | Description |
65
+ | -------------------- | ----------------------------- | ----------------------------------------------------------------- |
66
+ | `runType` | `GLCI_TEST_METRICS_RUN_TYPE` | Suite name (falls back to `CI_JOB_NAME` or `"unknown"`) |
67
+ | `observerUrl` | `GLCI_OBSERVER_URL` | Observer service base URL |
68
+ | `observerToken` | `GLCI_OBSERVER_AUTH_TOKEN` | Auth token sent as `X-Gitlab-Token` |
69
+ | `specFilePathPrefix` | _(none)_ | Prepended to file paths (useful for monorepos) |
70
+ | `extraMetadataKeys` | _(none)_ | Reserved; no-op until per-test metadata tagging lands |
71
+ | `skipRecord` | _(none)_ | `(testCase) => boolean`; return `true` to drop the record |
72
+ | `testRetried` | _(none)_ | `(testCase) => boolean`; return `true` if this run was a retry |
73
+ | `customMetrics` | _(none)_ | `(testCase) => object`; merged into every metric record |
74
+ | `logger` | _(none)_ | Logger-like object with `debug`/`info`/`warn`/`error` methods |
75
+
76
+ Two control-flow variables gate whether the reporter activates at all:
77
+
78
+ | Variable | Purpose |
79
+ | -------------------------- | ---------------------------------------------------------------- |
80
+ | `CI` | Must be set (any non-empty value) for the reporter to run |
81
+ | `GLCI_EXPORT_TEST_METRICS` | Set to `"false"` to disable export. Defaults to `"true"`. |
82
+
83
+ ### Activation requirements
84
+
85
+ The reporter is a no-op (i.e. does nothing, never crashes the run) unless
86
+ **all** of the following hold:
87
+
88
+ - `CI` is set.
89
+ - `GLCI_EXPORT_TEST_METRICS` is not `"false"`.
90
+ - `observerUrl` and `observerToken` are both resolved (from options or env).
91
+ The reporter logs a **warning** in this case.
92
+
93
+ ## Development
94
+
95
+ ```bash
96
+ pnpm install
97
+ pnpm test
98
+ pnpm run lint
99
+ ```
100
+
101
+ ## Release
102
+
103
+ The canonical version lives in [`src/version.ts`](src/version.ts). Bump it
104
+ there and open an MR. CI runs a `sync-package-version`
105
+ job on the MR that keeps `package.json`'s `version` field aligned by pushing
106
+ a sync commit back to the MR branch.
107
+
108
+ Once the MR is merged to the default branch, CI creates a `jest-<version>`
109
+ git tag automatically, which triggers a publish to the GitLab Package
110
+ Registry.
111
+
112
+ The sync-back step requires a `DEVELOPMENT_API_TOKEN` CI/CD variable with
113
+ `write_repository` scope.
114
+
115
+ ## License
116
+
117
+ MIT.
@@ -0,0 +1,16 @@
1
+ export declare class ResponseError extends Error {
2
+ readonly name = "ResponseError";
3
+ constructor(message: string);
4
+ }
5
+ export interface ClientOptions {
6
+ url: string;
7
+ token: string;
8
+ }
9
+ export declare class Client {
10
+ private readonly url;
11
+ private readonly token;
12
+ constructor({ url, token }: ClientOptions);
13
+ postTests(tests: readonly unknown[]): Promise<void>;
14
+ private postBatch;
15
+ }
16
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAOA,qBAAa,aAAc,SAAQ,KAAK;IACtC,SAAyB,IAAI,mBAAmB;gBAEpC,OAAO,EAAE,MAAM;CAG5B;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAOD,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAEnB,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,aAAa;IAKnC,SAAS,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YAS3C,SAAS;CA2CxB"}
package/dist/client.js ADDED
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Client = exports.ResponseError = void 0;
7
+ const node_http_1 = __importDefault(require("node:http"));
8
+ const node_https_1 = __importDefault(require("node:https"));
9
+ const node_url_1 = require("node:url");
10
+ const TESTS_PATH = '/api/v1/tests';
11
+ const MAX_BATCH_SIZE = 10_000;
12
+ class ResponseError extends Error {
13
+ name = 'ResponseError';
14
+ constructor(message) {
15
+ super(message);
16
+ }
17
+ }
18
+ exports.ResponseError = ResponseError;
19
+ class Client {
20
+ url;
21
+ token;
22
+ constructor({ url, token }) {
23
+ this.url = url;
24
+ this.token = token;
25
+ }
26
+ async postTests(tests) {
27
+ if (tests.length === 0)
28
+ return;
29
+ for (let i = 0; i < tests.length; i += MAX_BATCH_SIZE) {
30
+ const batch = tests.slice(i, i + MAX_BATCH_SIZE);
31
+ await this.postBatch(batch);
32
+ }
33
+ }
34
+ async postBatch(batch) {
35
+ const endpoint = new node_url_1.URL(`${this.url.replace(/\/$/, '')}${TESTS_PATH}`);
36
+ const transport = endpoint.protocol === 'https:' ? node_https_1.default : node_http_1.default;
37
+ const body = JSON.stringify({ tests: batch });
38
+ const response = await new Promise((resolve, reject) => {
39
+ const req = transport.request({
40
+ method: 'POST',
41
+ protocol: endpoint.protocol,
42
+ hostname: endpoint.hostname,
43
+ port: endpoint.port || (endpoint.protocol === 'https:' ? 443 : 80),
44
+ path: endpoint.pathname + endpoint.search,
45
+ headers: {
46
+ 'X-Gitlab-Token': this.token,
47
+ 'Content-Type': 'application/json',
48
+ 'Content-Length': Buffer.byteLength(body),
49
+ },
50
+ }, (res) => {
51
+ const chunks = [];
52
+ res.on('data', (chunk) => chunks.push(chunk));
53
+ res.on('end', () => {
54
+ resolve({
55
+ statusCode: res.statusCode ?? 0,
56
+ body: Buffer.concat(chunks).toString('utf8'),
57
+ });
58
+ });
59
+ res.on('error', reject);
60
+ });
61
+ req.on('error', reject);
62
+ req.write(body);
63
+ req.end();
64
+ });
65
+ if (response.statusCode < 200 || response.statusCode >= 300) {
66
+ throw new ResponseError(`Observer request failed with status ${String(response.statusCode)}: ${response.body}`);
67
+ }
68
+ }
69
+ }
70
+ exports.Client = Client;
71
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { ReporterOptions, ResolvedConfig } from './types';
2
+ export declare const LOG_PREFIX = "[MetricsExporter]";
3
+ export declare function resolveConfig(options: ReporterOptions): ResolvedConfig;
4
+ export interface ActivationResult {
5
+ disabled: boolean;
6
+ }
7
+ /**
8
+ * Decide whether the reporter should run for the current process.
9
+ *
10
+ * Side-effect: emits warnings via the resolved config's logger for missing
11
+ * configuration so misconfiguration is visible during CI runs.
12
+ */
13
+ export declare function computeActivation(config: ResolvedConfig): ActivationResult;
14
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAMvE,eAAO,MAAM,UAAU,sBAAsB,CAAC;AAE9C,wBAAgB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,cAAc,CAgBtE;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,gBAAgB,CAmB1E"}
package/dist/config.js ADDED
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LOG_PREFIX = void 0;
4
+ exports.resolveConfig = resolveConfig;
5
+ exports.computeActivation = computeActivation;
6
+ const DEFAULT_SKIP_RECORD = () => false;
7
+ const DEFAULT_TEST_RETRIED = () => false;
8
+ const DEFAULT_CUSTOM_METRICS = () => ({});
9
+ exports.LOG_PREFIX = '[MetricsExporter]';
10
+ function resolveConfig(options) {
11
+ return {
12
+ runType: options.runType ?? '',
13
+ observerUrl: options.observerUrl ?? process.env.GLCI_OBSERVER_URL ?? '',
14
+ observerToken: options.observerToken ?? process.env.GLCI_OBSERVER_AUTH_TOKEN ?? '',
15
+ specFilePathPrefix: options.specFilePathPrefix ?? '',
16
+ extraMetadataKeys: Array.isArray(options.extraMetadataKeys) ? options.extraMetadataKeys : [],
17
+ skipRecord: typeof options.skipRecord === 'function' ? options.skipRecord : DEFAULT_SKIP_RECORD,
18
+ testRetried: typeof options.testRetried === 'function' ? options.testRetried : DEFAULT_TEST_RETRIED,
19
+ customMetrics: typeof options.customMetrics === 'function'
20
+ ? options.customMetrics
21
+ : DEFAULT_CUSTOM_METRICS,
22
+ logger: options.logger ?? defaultLogger(),
23
+ };
24
+ }
25
+ /**
26
+ * Decide whether the reporter should run for the current process.
27
+ *
28
+ * Side-effect: emits warnings via the resolved config's logger for missing
29
+ * configuration so misconfiguration is visible during CI runs.
30
+ */
31
+ function computeActivation(config) {
32
+ if (!process.env.CI)
33
+ return { disabled: true };
34
+ if (process.env.GLCI_EXPORT_TEST_METRICS === 'false')
35
+ return { disabled: true };
36
+ if (!config.observerUrl) {
37
+ config.logger.warn(`${exports.LOG_PREFIX} Observer url is not configured. Set GLCI_OBSERVER_URL or pass observerUrl in the reporter options. Export disabled.`);
38
+ return { disabled: true };
39
+ }
40
+ if (!config.observerToken) {
41
+ config.logger.warn(`${exports.LOG_PREFIX} Observer auth token is not configured. Set GLCI_OBSERVER_AUTH_TOKEN or pass observerToken in the reporter options. Export disabled.`);
42
+ return { disabled: true };
43
+ }
44
+ return { disabled: false };
45
+ }
46
+ function defaultLogger() {
47
+ return {
48
+ debug: (msg) => console.debug(msg),
49
+ info: (msg) => console.info(msg),
50
+ warn: (msg) => console.warn(msg),
51
+ error: (msg) => console.error(msg),
52
+ };
53
+ }
54
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;AAQA,sCAgBC;AAYD,8CAmBC;AArDD,MAAM,mBAAmB,GAAG,GAAY,EAAE,CAAC,KAAK,CAAC;AACjD,MAAM,oBAAoB,GAAG,GAAY,EAAE,CAAC,KAAK,CAAC;AAClD,MAAM,sBAAsB,GAAG,GAA4B,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AAEtD,QAAA,UAAU,GAAG,mBAAmB,CAAC;AAE9C,SAAgB,aAAa,CAAC,OAAwB;IACpD,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE;QAC9B,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,EAAE;QACvE,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,EAAE;QAClF,kBAAkB,EAAE,OAAO,CAAC,kBAAkB,IAAI,EAAE;QACpD,iBAAiB,EAAE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE;QAC5F,UAAU,EAAE,OAAO,OAAO,CAAC,UAAU,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,mBAAmB;QAC/F,WAAW,EACT,OAAO,OAAO,CAAC,WAAW,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,oBAAoB;QACxF,aAAa,EACX,OAAO,OAAO,CAAC,aAAa,KAAK,UAAU;YACzC,CAAC,CAAC,OAAO,CAAC,aAAa;YACvB,CAAC,CAAC,sBAAsB;QAC5B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,aAAa,EAAE;KAC1C,CAAC;AACJ,CAAC;AAMD;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,MAAsB;IACtD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAI,OAAO,CAAC,GAAG,CAAC,wBAAwB,KAAK,OAAO;QAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAEhF,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QACxB,MAAM,CAAC,MAAM,CAAC,IAAI,CAChB,GAAG,kBAAU,sHAAsH,CACpI,CAAC;QACF,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC5B,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;QAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,CAChB,GAAG,kBAAU,sIAAsI,CACpJ,CAAC;QACF,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC5B,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,SAAS,aAAa;IACpB,OAAO;QACL,KAAK,EAAE,CAAC,GAAW,EAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC;QAChD,IAAI,EAAE,CAAC,GAAW,EAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;QAC9C,IAAI,EAAE,CAAC,GAAW,EAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;QAC9C,KAAK,EAAE,CAAC,GAAW,EAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC;KACjD,CAAC;AACJ,CAAC"}
package/dist/env.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export type EnvLike = Record<string, string | undefined>;
2
+ export declare function present(value: string | undefined): value is string;
3
+ export declare function parseIntOrUndefined(value: string | undefined): number | undefined;
4
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEzD,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,IAAI,MAAM,CAElE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIjF"}
package/dist/env.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.present = present;
4
+ exports.parseIntOrUndefined = parseIntOrUndefined;
5
+ function present(value) {
6
+ return typeof value === 'string' && value.length > 0;
7
+ }
8
+ function parseIntOrUndefined(value) {
9
+ if (!present(value))
10
+ return undefined;
11
+ const n = Number.parseInt(value, 10);
12
+ return Number.isNaN(n) ? undefined : n;
13
+ }
14
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":";;AAEA,0BAEC;AAED,kDAIC;AARD,SAAgB,OAAO,CAAC,KAAyB;IAC/C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;AACvD,CAAC;AAED,SAAgB,mBAAmB,CAAC,KAAyB;IAC3D,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACtC,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACrC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACzC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { JestMetricsReporter as ReporterImpl } from './reporter';
2
+ import type { CustomMetricsFn as CustomMetricsFnImpl, Logger as LoggerImpl, MetricRecord as MetricRecordImpl, MetricStatus as MetricStatusImpl, ReporterOptions as ReporterOptionsImpl, ResolvedConfig as ResolvedConfigImpl, SkipRecordFn as SkipRecordFnImpl, TestRetriedFn as TestRetriedFnImpl } from './types';
3
+ declare class JestMetricsReporter extends ReporterImpl {
4
+ }
5
+ declare namespace JestMetricsReporter {
6
+ type Logger = LoggerImpl;
7
+ type ReporterOptions = ReporterOptionsImpl;
8
+ type ResolvedConfig = ResolvedConfigImpl;
9
+ type MetricRecord = MetricRecordImpl;
10
+ type MetricStatus = MetricStatusImpl;
11
+ type SkipRecordFn = SkipRecordFnImpl;
12
+ type TestRetriedFn = TestRetriedFnImpl;
13
+ type CustomMetricsFn = CustomMetricsFnImpl;
14
+ const VERSION: "0.0.5";
15
+ }
16
+ export = JestMetricsReporter;
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,IAAI,YAAY,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,KAAK,EACV,eAAe,IAAI,mBAAmB,EACtC,MAAM,IAAI,UAAU,EACpB,YAAY,IAAI,gBAAgB,EAChC,YAAY,IAAI,gBAAgB,EAChC,eAAe,IAAI,mBAAmB,EACtC,cAAc,IAAI,kBAAkB,EACpC,YAAY,IAAI,gBAAgB,EAChC,aAAa,IAAI,iBAAiB,EACnC,MAAM,SAAS,CAAC;AAWjB,cAAM,mBAAoB,SAAQ,YAAY;CAAG;AAEjD,kBAAU,mBAAmB,CAAC;IAC5B,KAAY,MAAM,GAAG,UAAU,CAAC;IAChC,KAAY,eAAe,GAAG,mBAAmB,CAAC;IAClD,KAAY,cAAc,GAAG,kBAAkB,CAAC;IAChD,KAAY,YAAY,GAAG,gBAAgB,CAAC;IAC5C,KAAY,YAAY,GAAG,gBAAgB,CAAC;IAC5C,KAAY,YAAY,GAAG,gBAAgB,CAAC;IAC5C,KAAY,aAAa,GAAG,iBAAiB,CAAC;IAC9C,KAAY,eAAe,GAAG,mBAAmB,CAAC;IAC3C,MAAM,OAAO,SAAc,CAAC;CACpC;AAED,SAAS,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ const reporter_1 = require("./reporter");
3
+ const version_1 = require("./version");
4
+ // Jest historically loads reporters via `require(moduleName)` and uses the
5
+ // resulting export *itself* as a constructor. `export =` emits
6
+ // `module.exports = JestMetricsReporter` in the compiled JS, which is exactly
7
+ // what Jest's reporter loader expects.
8
+ //
9
+ // We re-declare the class via subclassing so it can be merged with a namespace
10
+ // in the same declaration scope, giving TypeScript consumers access to the
11
+ // shared types via `JestMetricsReporter.ReporterOptions` etc.
12
+ class JestMetricsReporter extends reporter_1.JestMetricsReporter {
13
+ }
14
+ // eslint-disable-next-line @typescript-eslint/no-namespace
15
+ (function (JestMetricsReporter) {
16
+ JestMetricsReporter.VERSION = version_1.VERSION;
17
+ })(JestMetricsReporter || (JestMetricsReporter = {}));
18
+ module.exports = JestMetricsReporter;
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,yCAAiE;AACjE,uCAAmD;AAYnD,2EAA2E;AAC3E,+DAA+D;AAC/D,8EAA8E;AAC9E,uCAAuC;AACvC,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,8DAA8D;AAE9D,MAAM,mBAAoB,SAAQ,8BAAY;CAAG;AACjD,2DAA2D;AAC3D,WAAU,mBAAmB;IASd,2BAAO,GAAG,iBAAW,CAAC;AACrC,CAAC,EAVS,mBAAmB,KAAnB,mBAAmB,QAU5B;AAED,iBAAS,mBAAmB,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { AggregatedResult, AssertionResult } from '@jest/test-result';
2
+ import type { MetricRecord, ResolvedConfig } from './types';
3
+ /**
4
+ * Compute the canonical batch timestamp.
5
+ *
6
+ * Uses CI_PIPELINE_CREATED_AT when set so every record across the run carries
7
+ * the same value. Format: `YYYY-MM-DDTHH:mm:ss.SSS000` (6 fractional digits,
8
+ * no UTC offset -- the Observer expects naive UTC).
9
+ */
10
+ export declare function computeTimestamp(): string;
11
+ export declare function buildMetricsFromAggregate(aggregated: Pick<AggregatedResult, 'testResults'>, config: ResolvedConfig, timestamp: string): MetricRecord[];
12
+ export declare function buildMetric(testCase: AssertionResult, testFilePath: string, timestamp: string, config: ResolvedConfig): MetricRecord;
13
+ //# sourceMappingURL=metric.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metric.d.ts","sourceRoot":"","sources":["../src/metric.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAc,MAAM,mBAAmB,CAAC;AAGvF,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAiB5D;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAIzC;AAED,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,IAAI,CAAC,gBAAgB,EAAE,aAAa,CAAC,EACjD,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,GAChB,YAAY,EAAE,CAShB;AAED,wBAAgB,WAAW,CACzB,QAAQ,EAAE,eAAe,EACzB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,GACrB,YAAY,CAuCd"}
package/dist/metric.js ADDED
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.computeTimestamp = computeTimestamp;
7
+ exports.buildMetricsFromAggregate = buildMetricsFromAggregate;
8
+ exports.buildMetric = buildMetric;
9
+ const node_crypto_1 = __importDefault(require("node:crypto"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const env_1 = require("./env");
12
+ const pipeline_1 = require("./pipeline");
13
+ const JEST_PARALLEL_SUFFIX = / \d{1,2}\/\d{1,2}$/;
14
+ const NEW_LINE = /\r?\n/g;
15
+ const FAILURE_MESSAGE_MAX_LEN = 1000;
16
+ const HASH_LENGTH = 41;
17
+ const STATUS_MAP = {
18
+ passed: 'passed',
19
+ failed: 'failed',
20
+ skipped: 'skipped',
21
+ pending: 'pending',
22
+ todo: 'pending',
23
+ disabled: 'skipped',
24
+ focused: 'passed',
25
+ };
26
+ /**
27
+ * Compute the canonical batch timestamp.
28
+ *
29
+ * Uses CI_PIPELINE_CREATED_AT when set so every record across the run carries
30
+ * the same value. Format: `YYYY-MM-DDTHH:mm:ss.SSS000` (6 fractional digits,
31
+ * no UTC offset -- the Observer expects naive UTC).
32
+ */
33
+ function computeTimestamp() {
34
+ const ciCreatedAt = process.env.CI_PIPELINE_CREATED_AT;
35
+ const date = ciCreatedAt ? new Date(ciCreatedAt) : new Date();
36
+ return formatTimestamp(date);
37
+ }
38
+ function buildMetricsFromAggregate(aggregated, config, timestamp) {
39
+ const records = [];
40
+ for (const fileResult of aggregated.testResults) {
41
+ for (const testCase of fileResult.testResults) {
42
+ if (config.skipRecord(testCase))
43
+ continue;
44
+ records.push(buildMetric(testCase, testFilePathOf(fileResult), timestamp, config));
45
+ }
46
+ }
47
+ return records;
48
+ }
49
+ function buildMetric(testCase, testFilePath, timestamp, config) {
50
+ const filePath = relativeFilePath(testFilePath);
51
+ const name = testCase.fullName || testCase.title || '';
52
+ const hash = node_crypto_1.default
53
+ .createHash('sha256')
54
+ .update(`${filePath}${name}`)
55
+ .digest('hex')
56
+ .slice(0, HASH_LENGTH);
57
+ const exceptionClasses = extractExceptionClasses(testCase.failureDetails);
58
+ const failureMessages = testCase.failureMessages ?? [];
59
+ const hasFailure = exceptionClasses.length > 0 || failureMessages.length > 0;
60
+ const failureException = hasFailure ? formatFailureMessage(failureMessages) : undefined;
61
+ const exceptionClass = exceptionClasses[0];
62
+ const record = {
63
+ timestamp,
64
+ id: `${filePath}:${name}`,
65
+ name,
66
+ hash,
67
+ file_path: filePath,
68
+ status: STATUS_MAP[testCase.status] ?? testCase.status,
69
+ run_time: normalizeDuration(testCase.duration),
70
+ location: locationFor(filePath, testCase.location ?? null),
71
+ exception_class: exceptionClass,
72
+ exception_classes: exceptionClasses,
73
+ failure_exception: failureException,
74
+ quarantined: false,
75
+ quarantine_issue_url: '',
76
+ feature_category: 'unknown',
77
+ test_retried: Boolean(config.testRetried(testCase)),
78
+ run_type: resolveRunType(config),
79
+ spec_file_path_prefix: config.specFilePathPrefix,
80
+ pipeline_type: (0, pipeline_1.detectPipelineType)(),
81
+ ...ciFields(),
82
+ ...coerceCustomMetrics(config.customMetrics(testCase)),
83
+ };
84
+ return dropNullish(record);
85
+ }
86
+ function testFilePathOf(result) {
87
+ // `testFilePath` is the canonical field in Jest 27+; fall back if absent.
88
+ return result.testFilePath;
89
+ }
90
+ function relativeFilePath(absolutePath) {
91
+ const rel = node_path_1.default.relative(process.cwd(), absolutePath);
92
+ return stripLeadingDotSlash(rel);
93
+ }
94
+ function stripLeadingDotSlash(value) {
95
+ return value.replace(/^\.\//, '');
96
+ }
97
+ function normalizeDuration(duration) {
98
+ if (duration === null || duration === undefined)
99
+ return 0;
100
+ const rounded = Math.round(duration);
101
+ // round up sub 1ms durations to avoid reporting 0ms runtime
102
+ return rounded === 0 ? 1 : rounded;
103
+ }
104
+ function locationFor(filePath, location) {
105
+ if (location && typeof location.line === 'number') {
106
+ return `${filePath}:${String(location.line)}`;
107
+ }
108
+ return filePath;
109
+ }
110
+ function extractExceptionClasses(failureDetails) {
111
+ if (!Array.isArray(failureDetails))
112
+ return [];
113
+ const names = [];
114
+ for (const detail of failureDetails) {
115
+ const name = exceptionClassName(detail);
116
+ if (name && !names.includes(name))
117
+ names.push(name);
118
+ }
119
+ return names;
120
+ }
121
+ function exceptionClassName(detail) {
122
+ if (!detail || typeof detail !== 'object')
123
+ return undefined;
124
+ if (detail.error?.constructor?.name) {
125
+ return detail.error.constructor.name;
126
+ }
127
+ if (typeof detail.matcherResult?.name === 'string') {
128
+ return detail.matcherResult.name;
129
+ }
130
+ return undefined;
131
+ }
132
+ function formatFailureMessage(failureMessages) {
133
+ const first = failureMessages[0];
134
+ if (!first)
135
+ return undefined;
136
+ return String(first).replace(NEW_LINE, ' ').slice(0, FAILURE_MESSAGE_MAX_LEN);
137
+ }
138
+ function ciFields() {
139
+ return {
140
+ ci_project_id: (0, env_1.parseIntOrUndefined)(env('CI_PROJECT_ID')),
141
+ ci_project_path: env('CI_PROJECT_PATH'),
142
+ ci_job_name: sanitizeJobName(env('CI_JOB_NAME')),
143
+ ci_job_id: (0, env_1.parseIntOrUndefined)(env('CI_JOB_ID')),
144
+ ci_pipeline_id: (0, pipeline_1.resolveCiPipelineId)(),
145
+ ci_merge_request_iid: (0, env_1.parseIntOrUndefined)(env('CI_MERGE_REQUEST_IID') ?? env('TOP_UPSTREAM_MERGE_REQUEST_IID')),
146
+ ci_branch: env('CI_COMMIT_REF_NAME'),
147
+ ci_target_branch: env('CI_MERGE_REQUEST_TARGET_BRANCH_NAME'),
148
+ ci_server_url: env('CI_SERVER_URL'),
149
+ };
150
+ }
151
+ function sanitizeJobName(jobName) {
152
+ if (!jobName)
153
+ return undefined;
154
+ return jobName.replace(JEST_PARALLEL_SUFFIX, '');
155
+ }
156
+ function resolveRunType(config) {
157
+ if (config.runType.length > 0)
158
+ return config.runType;
159
+ const envRunType = env('GLCI_TEST_METRICS_RUN_TYPE');
160
+ if (envRunType)
161
+ return envRunType;
162
+ const jobName = sanitizeJobName(env('CI_JOB_NAME'));
163
+ if (jobName)
164
+ return jobName;
165
+ return 'unknown';
166
+ }
167
+ function coerceCustomMetrics(metrics) {
168
+ if (!metrics || typeof metrics !== 'object')
169
+ return {};
170
+ const result = {};
171
+ for (const [key, value] of Object.entries(metrics)) {
172
+ result[key] = coerceScalar(value);
173
+ }
174
+ return result;
175
+ }
176
+ function coerceScalar(value) {
177
+ if (value === null)
178
+ return null;
179
+ const t = typeof value;
180
+ if (t === 'string' || t === 'number' || t === 'boolean')
181
+ return value;
182
+ // Anything non-scalar gets stringified to match the documented contract.
183
+ // Use the explicit `Object.prototype.toString`-style call so the result is
184
+ // deterministic for arbitrary inputs.
185
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
186
+ return String(value);
187
+ }
188
+ function dropNullish(record) {
189
+ const result = {};
190
+ for (const [k, v] of Object.entries(record)) {
191
+ if (v === null || v === undefined)
192
+ continue;
193
+ result[k] = v;
194
+ }
195
+ return result;
196
+ }
197
+ function env(name) {
198
+ const value = process.env[name];
199
+ return (0, env_1.present)(value) ? value : undefined;
200
+ }
201
+ function formatTimestamp(date) {
202
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
203
+ const yyyy = date.getUTCFullYear();
204
+ const mm = pad(date.getUTCMonth() + 1);
205
+ const dd = pad(date.getUTCDate());
206
+ const HH = pad(date.getUTCHours());
207
+ const MM = pad(date.getUTCMinutes());
208
+ const SS = pad(date.getUTCSeconds());
209
+ const ms = pad(date.getUTCMilliseconds(), 3);
210
+ return `${String(yyyy)}-${mm}-${dd}T${HH}:${MM}:${SS}.${ms}000`;
211
+ }
212
+ //# sourceMappingURL=metric.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metric.js","sourceRoot":"","sources":["../src/metric.ts"],"names":[],"mappings":";;;;;AA6BA,4CAIC;AAED,8DAaC;AAED,kCA4CC;AA9FD,8DAAiC;AACjC,0DAA6B;AAE7B,+BAAqD;AACrD,yCAAqE;AAGrE,MAAM,oBAAoB,GAAG,oBAAoB,CAAC;AAClD,MAAM,QAAQ,GAAG,QAAQ,CAAC;AAC1B,MAAM,uBAAuB,GAAG,IAAI,CAAC;AACrC,MAAM,WAAW,GAAG,EAAE,CAAC;AAEvB,MAAM,UAAU,GAA2B;IACzC,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,SAAS;IACf,QAAQ,EAAE,SAAS;IACnB,OAAO,EAAE,QAAQ;CAClB,CAAC;AAEF;;;;;;GAMG;AACH,SAAgB,gBAAgB;IAC9B,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC;IACvD,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;IAC9D,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC;AAED,SAAgB,yBAAyB,CACvC,UAAiD,EACjD,MAAsB,EACtB,SAAiB;IAEjB,MAAM,OAAO,GAAmB,EAAE,CAAC;IACnC,KAAK,MAAM,UAAU,IAAI,UAAU,CAAC,WAAW,EAAE,CAAC;QAChD,KAAK,MAAM,QAAQ,IAAI,UAAU,CAAC,WAAW,EAAE,CAAC;YAC9C,IAAI,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAC1C,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAgB,WAAW,CACzB,QAAyB,EACzB,YAAoB,EACpB,SAAiB,EACjB,MAAsB;IAEtB,MAAM,QAAQ,GAAG,gBAAgB,CAAC,YAAY,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,IAAI,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC;IACvD,MAAM,IAAI,GAAG,qBAAM;SAChB,UAAU,CAAC,QAAQ,CAAC;SACpB,MAAM,CAAC,GAAG,QAAQ,GAAG,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,KAAK,CAAC;SACb,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;IAEzB,MAAM,gBAAgB,GAAG,uBAAuB,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IAC1E,MAAM,eAAe,GAAG,QAAQ,CAAC,eAAe,IAAI,EAAE,CAAC;IACvD,MAAM,UAAU,GAAG,gBAAgB,CAAC,MAAM,GAAG,CAAC,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC;IAC7E,MAAM,gBAAgB,GAAG,UAAU,CAAC,CAAC,CAAC,oBAAoB,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACxF,MAAM,cAAc,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAE3C,MAAM,MAAM,GAAiB;QAC3B,SAAS;QACT,EAAE,EAAE,GAAG,QAAQ,IAAI,IAAI,EAAE;QACzB,IAAI;QACJ,IAAI;QACJ,SAAS,EAAE,QAAQ;QACnB,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,MAAM;QACtD,QAAQ,EAAE,iBAAiB,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAC9C,QAAQ,EAAE,WAAW,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,IAAI,IAAI,CAAC;QAC1D,eAAe,EAAE,cAAc;QAC/B,iBAAiB,EAAE,gBAAgB;QACnC,iBAAiB,EAAE,gBAAgB;QACnC,WAAW,EAAE,KAAK;QAClB,oBAAoB,EAAE,EAAE;QACxB,gBAAgB,EAAE,SAAS;QAC3B,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QACnD,QAAQ,EAAE,cAAc,CAAC,MAAM,CAAC;QAChC,qBAAqB,EAAE,MAAM,CAAC,kBAAkB;QAChD,aAAa,EAAE,IAAA,6BAAkB,GAAE;QACnC,GAAG,QAAQ,EAAE;QACb,GAAG,mBAAmB,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;KACvD,CAAC;IAEF,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,cAAc,CAAC,MAAkB;IACxC,0EAA0E;IAC1E,OAAO,MAAM,CAAC,YAAY,CAAC;AAC7B,CAAC;AAED,SAAS,gBAAgB,CAAC,YAAoB;IAC5C,MAAM,GAAG,GAAG,mBAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,CAAC;IACvD,OAAO,oBAAoB,CAAC,GAAG,CAAC,CAAC;AACnC,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAa;IACzC,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AACpC,CAAC;AAED,SAAS,iBAAiB,CAAC,QAAmC;IAC5D,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAErC,4DAA4D;IAC5D,OAAO,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AACrC,CAAC;AAED,SAAS,WAAW,CAClB,QAAgB,EAChB,QAAmD;IAEnD,IAAI,QAAQ,IAAI,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAClD,OAAO,GAAG,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IAChD,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAOD,SAAS,uBAAuB,CAAC,cAAuB;IACtD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC;QAAE,OAAO,EAAE,CAAC;IAE9C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,MAAM,IAAI,cAAiC,EAAE,CAAC;QACvD,MAAM,IAAI,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;QACxC,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,kBAAkB,CAAC,MAAwC;IAClE,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QACpC,OAAO,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC;IACvC,CAAC;IACD,IAAI,OAAO,MAAM,CAAC,aAAa,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;QACnD,OAAO,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;IACnC,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,oBAAoB,CAAC,eAAkC;IAC9D,MAAM,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;IACjC,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,uBAAuB,CAAC,CAAC;AAChF,CAAC;AAcD,SAAS,QAAQ;IACf,OAAO;QACL,aAAa,EAAE,IAAA,yBAAmB,EAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACxD,eAAe,EAAE,GAAG,CAAC,iBAAiB,CAAC;QACvC,WAAW,EAAE,eAAe,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAChD,SAAS,EAAE,IAAA,yBAAmB,EAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAChD,cAAc,EAAE,IAAA,8BAAmB,GAAE;QACrC,oBAAoB,EAAE,IAAA,yBAAmB,EACvC,GAAG,CAAC,sBAAsB,CAAC,IAAI,GAAG,CAAC,gCAAgC,CAAC,CACrE;QACD,SAAS,EAAE,GAAG,CAAC,oBAAoB,CAAC;QACpC,gBAAgB,EAAE,GAAG,CAAC,qCAAqC,CAAC;QAC5D,aAAa,EAAE,GAAG,CAAC,eAAe,CAAC;KACpC,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,OAA2B;IAClD,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAC/B,OAAO,OAAO,CAAC,OAAO,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,cAAc,CAAC,MAAsB;IAC5C,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,MAAM,CAAC,OAAO,CAAC;IAErD,MAAM,UAAU,GAAG,GAAG,CAAC,4BAA4B,CAAC,CAAC;IACrD,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IAElC,MAAM,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC;IACpD,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAE5B,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,mBAAmB,CAAC,OAAmD;IAC9E,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACvD,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,MAAM,CAAC,GAAG,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,CAAC,GAAG,OAAO,KAAK,CAAC;IACvB,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtE,yEAAyE;IACzE,2EAA2E;IAC3E,sCAAsC;IACtC,gEAAgE;IAChE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED,SAAS,WAAW,CAAC,MAAoB;IACvC,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5C,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,SAAS;YAAE,SAAS;QAC5C,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAChB,CAAC;IACD,OAAO,MAAsB,CAAC;AAChC,CAAC;AAED,SAAS,GAAG,CAAC,IAAY;IACvB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,OAAO,IAAA,aAAO,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5C,CAAC;AAED,SAAS,eAAe,CAAC,IAAU;IACjC,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,CAAC,GAAG,CAAC,EAAU,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACrE,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;IACnC,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC;IACvC,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;IAClC,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IACnC,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC;IACrC,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC;IACrC,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC,CAAC;IAC7C,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC;AAClE,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { type EnvLike } from './env';
2
+ export declare function detectPipelineType(env?: EnvLike): string;
3
+ export declare function resolveCiPipelineId(env?: EnvLike): number | undefined;
4
+ //# sourceMappingURL=pipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,OAAO,EAAE,MAAM,OAAO,CAAC;AAInE,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,OAAqB,GAAG,MAAM,CAyBrE;AAED,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,OAAqB,GAAG,MAAM,GAAG,SAAS,CAIlF"}
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectPipelineType = detectPipelineType;
4
+ exports.resolveCiPipelineId = resolveCiPipelineId;
5
+ const env_1 = require("./env");
6
+ const STABLE_EE_BRANCH_REGEX = /^[\d-]+-stable-ee$/;
7
+ function detectPipelineType(env = process.env) {
8
+ const ref = env.CI_COMMIT_REF_NAME;
9
+ const defaultBranch = env.CI_DEFAULT_BRANCH;
10
+ const onDefaultBranch = (0, env_1.present)(ref) && ref === defaultBranch;
11
+ if (onDefaultBranch && (0, env_1.present)(env.SCHEDULE_TYPE)) {
12
+ return 'default_branch_scheduled_pipeline';
13
+ }
14
+ if (onDefaultBranch) {
15
+ return 'default_branch_pipeline';
16
+ }
17
+ if ((0, env_1.present)(ref) && STABLE_EE_BRANCH_REGEX.test(ref)) {
18
+ return 'stable_branch_pipeline';
19
+ }
20
+ const targetBranch = env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
21
+ if ((0, env_1.present)(targetBranch) && STABLE_EE_BRANCH_REGEX.test(targetBranch)) {
22
+ return 'backport_merge_request_pipeline';
23
+ }
24
+ if ((0, env_1.present)(env.CI_MERGE_REQUEST_IID)) {
25
+ return 'merge_request_pipeline';
26
+ }
27
+ if (env.CI_PIPELINE_SOURCE === 'pipeline') {
28
+ return 'downstream_pipeline';
29
+ }
30
+ return 'unknown';
31
+ }
32
+ function resolveCiPipelineId(env = process.env) {
33
+ const parent = (0, env_1.parseIntOrUndefined)(env.PARENT_PIPELINE_ID);
34
+ if (parent !== undefined)
35
+ return parent;
36
+ return (0, env_1.parseIntOrUndefined)(env.CI_PIPELINE_ID);
37
+ }
38
+ //# sourceMappingURL=pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":";;AAIA,gDAyBC;AAED,kDAIC;AAnCD,+BAAmE;AAEnE,MAAM,sBAAsB,GAAG,oBAAoB,CAAC;AAEpD,SAAgB,kBAAkB,CAAC,MAAe,OAAO,CAAC,GAAG;IAC3D,MAAM,GAAG,GAAG,GAAG,CAAC,kBAAkB,CAAC;IACnC,MAAM,aAAa,GAAG,GAAG,CAAC,iBAAiB,CAAC;IAC5C,MAAM,eAAe,GAAG,IAAA,aAAO,EAAC,GAAG,CAAC,IAAI,GAAG,KAAK,aAAa,CAAC;IAE9D,IAAI,eAAe,IAAI,IAAA,aAAO,EAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;QAClD,OAAO,mCAAmC,CAAC;IAC7C,CAAC;IACD,IAAI,eAAe,EAAE,CAAC;QACpB,OAAO,yBAAyB,CAAC;IACnC,CAAC;IACD,IAAI,IAAA,aAAO,EAAC,GAAG,CAAC,IAAI,sBAAsB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACrD,OAAO,wBAAwB,CAAC;IAClC,CAAC;IACD,MAAM,YAAY,GAAG,GAAG,CAAC,mCAAmC,CAAC;IAC7D,IAAI,IAAA,aAAO,EAAC,YAAY,CAAC,IAAI,sBAAsB,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QACvE,OAAO,iCAAiC,CAAC;IAC3C,CAAC;IACD,IAAI,IAAA,aAAO,EAAC,GAAG,CAAC,oBAAoB,CAAC,EAAE,CAAC;QACtC,OAAO,wBAAwB,CAAC;IAClC,CAAC;IACD,IAAI,GAAG,CAAC,kBAAkB,KAAK,UAAU,EAAE,CAAC;QAC1C,OAAO,qBAAqB,CAAC;IAC/B,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAgB,mBAAmB,CAAC,MAAe,OAAO,CAAC,GAAG;IAC5D,MAAM,MAAM,GAAG,IAAA,yBAAmB,EAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAC3D,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACxC,OAAO,IAAA,yBAAmB,EAAC,GAAG,CAAC,cAAc,CAAC,CAAC;AACjD,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { Config, Reporter, ReporterOnStartOptions, TestContext } from '@jest/reporters';
2
+ import type { AggregatedResult, Test, TestResult } from '@jest/test-result';
3
+ import type { Logger, ReporterOptions, ResolvedConfig } from './types';
4
+ export declare class JestMetricsReporter implements Reporter {
5
+ private readonly config;
6
+ private readonly disabled;
7
+ private readonly client;
8
+ constructor(_globalConfig: Config.GlobalConfig, reporterOptions?: ReporterOptions);
9
+ onRunStart(_aggregatedResults: AggregatedResult, _options: ReporterOnStartOptions): void;
10
+ onTestStart(_test: Test): void;
11
+ onTestResult(_test: Test, _testResult: TestResult, _aggregatedResults: AggregatedResult): void;
12
+ onRunComplete(_testContexts: Set<TestContext>, aggregatedResults: AggregatedResult): Promise<void>;
13
+ getLastError(): Error | undefined;
14
+ /** @internal */
15
+ get _config(): ResolvedConfig;
16
+ /** @internal */
17
+ get _disabled(): boolean;
18
+ /** @internal */
19
+ get _logger(): Logger;
20
+ }
21
+ export default JestMetricsReporter;
22
+ //# sourceMappingURL=reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC7F,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAI5E,OAAO,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEvE,qBAAa,mBAAoB,YAAW,QAAQ;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;gBAEhC,aAAa,EAAE,MAAM,CAAC,YAAY,EAAE,eAAe,GAAE,eAAoB;IA0BrF,UAAU,CACR,kBAAkB,EAAE,gBAAgB,EACpC,QAAQ,EAAE,sBAAsB,GAC/B,IAAI;IASP,WAAW,CAAC,KAAK,EAAE,IAAI,GAAG,IAAI;IAI9B,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,kBAAkB,EAAE,gBAAgB,GAAG,IAAI;IAIxF,aAAa,CACjB,aAAa,EAAE,GAAG,CAAC,WAAW,CAAC,EAC/B,iBAAiB,EAAE,gBAAgB,GAClC,OAAO,CAAC,IAAI,CAAC;IAiChB,YAAY,IAAI,KAAK,GAAG,SAAS;IAKjC,gBAAgB;IAChB,IAAI,OAAO,IAAI,cAAc,CAE5B;IAED,gBAAgB;IAChB,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,gBAAgB;IAChB,IAAI,OAAO,IAAI,MAAM,CAEpB;CACF;AAOD,eAAe,mBAAmB,CAAC"}
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JestMetricsReporter = void 0;
4
+ const client_1 = require("./client");
5
+ const config_1 = require("./config");
6
+ const metric_1 = require("./metric");
7
+ class JestMetricsReporter {
8
+ config;
9
+ disabled;
10
+ client;
11
+ constructor(_globalConfig, reporterOptions = {}) {
12
+ let resolved;
13
+ let disabled = false;
14
+ let client;
15
+ try {
16
+ resolved = (0, config_1.resolveConfig)(reporterOptions);
17
+ disabled = (0, config_1.computeActivation)(resolved).disabled;
18
+ if (!disabled) {
19
+ client = new client_1.Client({ url: resolved.observerUrl, token: resolved.observerToken });
20
+ }
21
+ }
22
+ catch (err) {
23
+ resolved = (0, config_1.resolveConfig)({ logger: reporterOptions.logger });
24
+ disabled = true;
25
+ const logger = reporterOptions.logger ?? resolved.logger;
26
+ logger.error(`${config_1.LOG_PREFIX} Reporter initialization failed: ${describeError(err)}`);
27
+ }
28
+ this.config = resolved;
29
+ this.disabled = disabled;
30
+ this.client = client;
31
+ }
32
+ // Required by Jest's Reporter interface. We do all the work in onRunComplete,
33
+ // so these are intentional no-ops kept around for type compatibility and to
34
+ // emit a one-time diagnostic about the no-op `extraMetadataKeys` option.
35
+ onRunStart(_aggregatedResults, _options) {
36
+ if (this.disabled)
37
+ return;
38
+ if (this.config.extraMetadataKeys.length > 0) {
39
+ this.config.logger.debug(`${config_1.LOG_PREFIX} extraMetadataKeys is a no-op until per-test metadata tagging is implemented`);
40
+ }
41
+ }
42
+ onTestStart(_test) {
43
+ /* no-op */
44
+ }
45
+ onTestResult(_test, _testResult, _aggregatedResults) {
46
+ /* no-op */
47
+ }
48
+ async onRunComplete(_testContexts, aggregatedResults) {
49
+ if (this.disabled || !this.client)
50
+ return;
51
+ const timestamp = (0, metric_1.computeTimestamp)();
52
+ let records;
53
+ try {
54
+ records = (0, metric_1.buildMetricsFromAggregate)(aggregatedResults, this.config, timestamp);
55
+ }
56
+ catch (err) {
57
+ this.config.logger.error(`${config_1.LOG_PREFIX} Error while building metrics: ${describeError(err)}`);
58
+ return;
59
+ }
60
+ if (records.length === 0) {
61
+ this.config.logger.warn(`${config_1.LOG_PREFIX} No test execution records found, metrics will not be exported`);
62
+ return;
63
+ }
64
+ try {
65
+ await this.client.postTests(records);
66
+ this.config.logger.info(`${config_1.LOG_PREFIX} Successfully pushed ${String(records.length)} entries to Observer`);
67
+ }
68
+ catch (err) {
69
+ this.config.logger.error(`${config_1.LOG_PREFIX} Error occurred while pushing metrics to Observer: ${describeError(err)}`);
70
+ }
71
+ }
72
+ getLastError() {
73
+ return undefined;
74
+ }
75
+ // Exposed for testing.
76
+ /** @internal */
77
+ get _config() {
78
+ return this.config;
79
+ }
80
+ /** @internal */
81
+ get _disabled() {
82
+ return this.disabled;
83
+ }
84
+ /** @internal */
85
+ get _logger() {
86
+ return this.config.logger;
87
+ }
88
+ }
89
+ exports.JestMetricsReporter = JestMetricsReporter;
90
+ function describeError(err) {
91
+ if (err instanceof Error)
92
+ return err.message;
93
+ return String(err);
94
+ }
95
+ exports.default = JestMetricsReporter;
96
+ //# sourceMappingURL=reporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.js","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":";;;AAEA,qCAAkC;AAClC,qCAAwE;AACxE,qCAAuE;AAGvE,MAAa,mBAAmB;IACb,MAAM,CAAiB;IACvB,QAAQ,CAAU;IAClB,MAAM,CAAqB;IAE5C,YAAY,aAAkC,EAAE,kBAAmC,EAAE;QACnF,IAAI,QAAwB,CAAC;QAC7B,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,MAA0B,CAAC;QAE/B,IAAI,CAAC;YACH,QAAQ,GAAG,IAAA,sBAAa,EAAC,eAAe,CAAC,CAAC;YAC1C,QAAQ,GAAG,IAAA,0BAAiB,EAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;YAChD,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,GAAG,IAAI,eAAM,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,WAAW,EAAE,KAAK,EAAE,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC;YACpF,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,QAAQ,GAAG,IAAA,sBAAa,EAAC,EAAE,MAAM,EAAE,eAAe,CAAC,MAAM,EAAE,CAAC,CAAC;YAC7D,QAAQ,GAAG,IAAI,CAAC;YAChB,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC;YACzD,MAAM,CAAC,KAAK,CAAC,GAAG,mBAAU,oCAAoC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACtF,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,8EAA8E;IAC9E,4EAA4E;IAC5E,yEAAyE;IACzE,UAAU,CACR,kBAAoC,EACpC,QAAgC;QAEhC,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC1B,IAAI,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7C,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CACtB,GAAG,mBAAU,8EAA8E,CAC5F,CAAC;QACJ,CAAC;IACH,CAAC;IAED,WAAW,CAAC,KAAW;QACrB,WAAW;IACb,CAAC;IAED,YAAY,CAAC,KAAW,EAAE,WAAuB,EAAE,kBAAoC;QACrF,WAAW;IACb,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,aAA+B,EAC/B,iBAAmC;QAEnC,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO;QAE1C,MAAM,SAAS,GAAG,IAAA,yBAAgB,GAAE,CAAC;QACrC,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,IAAA,kCAAyB,EAAC,iBAAiB,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QACjF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CACtB,GAAG,mBAAU,kCAAkC,aAAa,CAAC,GAAG,CAAC,EAAE,CACpE,CAAC;YACF,OAAO;QACT,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CACrB,GAAG,mBAAU,gEAAgE,CAC9E,CAAC;YACF,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;YACrC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CACrB,GAAG,mBAAU,wBAAwB,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,sBAAsB,CAClF,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CACtB,GAAG,mBAAU,sDAAsD,aAAa,CAAC,GAAG,CAAC,EAAE,CACxF,CAAC;QACJ,CAAC;IACH,CAAC;IAED,YAAY;QACV,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,uBAAuB;IACvB,gBAAgB;IAChB,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED,gBAAgB;IAChB,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,gBAAgB;IAChB,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC5B,CAAC;CACF;AA1GD,kDA0GC;AAED,SAAS,aAAa,CAAC,GAAY;IACjC,IAAI,GAAG,YAAY,KAAK;QAAE,OAAO,GAAG,CAAC,OAAO,CAAC;IAC7C,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;AACrB,CAAC;AAED,kBAAe,mBAAmB,CAAC"}
@@ -0,0 +1,64 @@
1
+ import type { AssertionResult } from '@jest/test-result';
2
+ export interface Logger {
3
+ debug(message: string): void;
4
+ info(message: string): void;
5
+ warn(message: string): void;
6
+ error(message: string): void;
7
+ }
8
+ export type SkipRecordFn = (testCase: AssertionResult) => boolean;
9
+ export type TestRetriedFn = (testCase: AssertionResult) => boolean;
10
+ export type CustomMetricsFn = (testCase: AssertionResult) => Record<string, unknown>;
11
+ export interface ReporterOptions {
12
+ runType?: string;
13
+ observerUrl?: string;
14
+ observerToken?: string;
15
+ specFilePathPrefix?: string;
16
+ extraMetadataKeys?: string[];
17
+ skipRecord?: SkipRecordFn;
18
+ testRetried?: TestRetriedFn;
19
+ customMetrics?: CustomMetricsFn;
20
+ logger?: Logger;
21
+ }
22
+ export interface ResolvedConfig {
23
+ runType: string;
24
+ observerUrl: string;
25
+ observerToken: string;
26
+ specFilePathPrefix: string;
27
+ extraMetadataKeys: string[];
28
+ skipRecord: SkipRecordFn;
29
+ testRetried: TestRetriedFn;
30
+ customMetrics: CustomMetricsFn;
31
+ logger: Logger;
32
+ }
33
+ export type MetricStatus = 'passed' | 'failed' | 'pending' | 'skipped';
34
+ export interface MetricRecord {
35
+ timestamp: string;
36
+ id: string;
37
+ name: string;
38
+ hash: string;
39
+ file_path: string;
40
+ status: string;
41
+ run_time: number;
42
+ location: string;
43
+ exception_class?: string;
44
+ exception_classes: string[];
45
+ failure_exception?: string;
46
+ quarantined: boolean;
47
+ quarantine_issue_url: string;
48
+ feature_category: string;
49
+ test_retried: boolean;
50
+ run_type: string;
51
+ spec_file_path_prefix: string;
52
+ pipeline_type: string;
53
+ ci_project_id?: number;
54
+ ci_project_path?: string;
55
+ ci_job_name?: string;
56
+ ci_job_id?: number;
57
+ ci_pipeline_id?: number;
58
+ ci_merge_request_iid?: number;
59
+ ci_branch?: string;
60
+ ci_target_branch?: string;
61
+ ci_server_url?: string;
62
+ [customKey: string]: unknown;
63
+ }
64
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,eAAe,KAAK,OAAO,CAAC;AAClE,MAAM,MAAM,aAAa,GAAG,CAAC,QAAQ,EAAE,eAAe,KAAK,OAAO,CAAC;AACnE,MAAM,MAAM,eAAe,GAAG,CAAC,QAAQ,EAAE,eAAe,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAErF,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B,aAAa,CAAC,EAAE,eAAe,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,UAAU,EAAE,YAAY,CAAC;IACzB,WAAW,EAAE,aAAa,CAAC;IAC3B,aAAa,EAAE,eAAe,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvE,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAElB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CAC9B"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export declare const VERSION: "0.0.5";
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,EAAG,OAAgB,CAAC"}
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VERSION = void 0;
4
+ exports.VERSION = '0.0.5';
5
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":";;;AAAa,QAAA,OAAO,GAAG,OAAgB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@gitlab/jest-metrics-exporter",
3
+ "version": "0.0.5",
4
+ "description": "A Jest reporter that collects test execution data and pushes it to the GitLab Observer service",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist/",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://gitlab.com/gitlab-org/quality/analytics/test-metrics-exporters"
15
+ },
16
+ "homepage": "https://gitlab.com/gitlab-org/quality/analytics/test-metrics-exporters",
17
+ "license": "MIT",
18
+ "author": "Developer Experience",
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "peerDependencies": {
23
+ "jest": ">=29"
24
+ },
25
+ "devDependencies": {
26
+ "@eslint/js": "^9.16.0",
27
+ "@jest/reporters": "^29.7.0",
28
+ "@jest/test-result": "^29.7.0",
29
+ "@types/jest": "^29.5.14",
30
+ "@types/node": "^20.17.10",
31
+ "eslint": "^9.16.0",
32
+ "eslint-config-prettier": "^9.1.0",
33
+ "jest": "^29.7.0",
34
+ "prettier": "^3.4.2",
35
+ "ts-jest": "^29.2.5",
36
+ "typescript": "^5.6.3",
37
+ "typescript-eslint": "^8.18.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc",
41
+ "clean": "rm -rf dist",
42
+ "lint": "eslint src test",
43
+ "lint:fix": "eslint src test --fix",
44
+ "format": "prettier --write src test",
45
+ "test": "jest",
46
+ "typecheck": "tsc --noEmit"
47
+ }
48
+ }