@sentinelqa/playwright-reporter 0.1.45 → 0.1.50
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/localReport.js +34 -11
- package/dist/quickDiagnosis.d.ts +68 -4
- package/dist/quickDiagnosis.js +813 -97
- package/dist/reporter.d.ts +1 -0
- package/dist/reporter.js +103 -12
- package/dist/runHistory.d.ts +4 -0
- package/dist/runHistory.js +113 -3
- package/dist/telemetry.d.ts +3 -0
- package/dist/telemetry.js +173 -0
- package/dist/terminalSummary.d.ts +31 -0
- package/dist/terminalSummary.js +398 -0
- package/package.json +2 -2
package/dist/reporter.d.ts
CHANGED
package/dist/reporter.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
const node_1 = require("@sentinelqa/uploader/node");
|
|
3
3
|
const env_1 = require("./env");
|
|
4
4
|
const quickDiagnosis_1 = require("./quickDiagnosis");
|
|
5
|
+
const terminalSummary_1 = require("./terminalSummary");
|
|
6
|
+
const runHistory_1 = require("./runHistory");
|
|
7
|
+
const telemetry_1 = require("./telemetry");
|
|
5
8
|
const { sentinelCaptureFailureContextFromReporter } = require("@sentinelqa/uploader/playwright");
|
|
6
9
|
const colorize = (value, code) => {
|
|
7
10
|
if (!process.stdout.isTTY)
|
|
@@ -11,14 +14,43 @@ const colorize = (value, code) => {
|
|
|
11
14
|
const green = (value) => colorize(value, "32");
|
|
12
15
|
const yellow = (value) => colorize(value, "33");
|
|
13
16
|
const dim = (value) => colorize(value, "2");
|
|
17
|
+
const readFinalFailedCount = (playwrightJsonPath) => {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(require("node:fs").readFileSync(playwrightJsonPath, "utf8"));
|
|
20
|
+
let failed = 0;
|
|
21
|
+
const walk = (node) => {
|
|
22
|
+
if (!node)
|
|
23
|
+
return;
|
|
24
|
+
for (const child of node.suites || [])
|
|
25
|
+
walk(child);
|
|
26
|
+
for (const child of node.specs || [])
|
|
27
|
+
walk(child);
|
|
28
|
+
for (const test of node.tests || []) {
|
|
29
|
+
const results = Array.isArray(test.results) ? test.results : [];
|
|
30
|
+
const finalStatus = results[results.length - 1]?.status || null;
|
|
31
|
+
if (finalStatus === "failed" || finalStatus === "timedOut" || finalStatus === "interrupted") {
|
|
32
|
+
failed += 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
walk(parsed);
|
|
37
|
+
return failed;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
14
43
|
class SentinelReporter {
|
|
15
44
|
constructor(options) {
|
|
16
45
|
this.failedCount = 0;
|
|
17
46
|
this.totalCount = 0;
|
|
47
|
+
this.startedAt = Date.now();
|
|
18
48
|
(0, env_1.loadSentinelEnv)();
|
|
19
49
|
this.options = options;
|
|
20
50
|
}
|
|
21
51
|
onBegin(config, suite) {
|
|
52
|
+
this.startedAt = Date.now();
|
|
53
|
+
(0, telemetry_1.emitReporterTelemetry)();
|
|
22
54
|
this.totalCount = typeof suite?.allTests === "function" ? suite.allTests().length : 0;
|
|
23
55
|
if (config?.projects?.length && !this.options.project) {
|
|
24
56
|
this.options.project = config.projects[0]?.name || null;
|
|
@@ -50,14 +82,41 @@ class SentinelReporter {
|
|
|
50
82
|
!hasCiEnv &&
|
|
51
83
|
!localUploadEnabled;
|
|
52
84
|
const quickDiagnosis = (0, quickDiagnosis_1.buildQuickDiagnosis)(this.options.playwrightJsonPath);
|
|
85
|
+
const finalFailedCount = readFinalFailedCount(this.options.playwrightJsonPath);
|
|
86
|
+
const effectiveFailedCount = typeof finalFailedCount === "number" ? finalFailedCount : this.failedCount;
|
|
87
|
+
const passingSummary = effectiveFailedCount === 0
|
|
88
|
+
? (0, terminalSummary_1.buildPassingRunSummary)(this.options.playwrightJsonPath, { observedRunDurationMs: Date.now() - this.startedAt })
|
|
89
|
+
: null;
|
|
90
|
+
const failedRunHistory = effectiveFailedCount > 0 ? (0, runHistory_1.buildFailedRunHistorySummary)(this.options.playwrightJsonPath) : null;
|
|
91
|
+
if (effectiveFailedCount > 0) {
|
|
92
|
+
(0, terminalSummary_1.recordReporterHistorySnapshot)(this.options.playwrightJsonPath);
|
|
93
|
+
}
|
|
53
94
|
console.log("");
|
|
54
|
-
if (
|
|
55
|
-
console.log(
|
|
56
|
-
for (const line of
|
|
57
|
-
console.log(` ${
|
|
95
|
+
if (passingSummary) {
|
|
96
|
+
console.log(green("Sentinel run summary"));
|
|
97
|
+
for (const line of passingSummary.lines) {
|
|
98
|
+
console.log(` ${line}`);
|
|
99
|
+
}
|
|
100
|
+
if (passingSummary.risks.length > 0) {
|
|
101
|
+
console.log("");
|
|
102
|
+
console.log(yellow("Potential risks"));
|
|
103
|
+
for (const line of passingSummary.risks) {
|
|
104
|
+
console.log(` ${line}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!hasWorkspaceToken) {
|
|
108
|
+
console.log("");
|
|
109
|
+
console.log("Activate a free workspace at sentinelqa.com/register");
|
|
58
110
|
}
|
|
59
111
|
console.log("");
|
|
60
112
|
}
|
|
113
|
+
if (effectiveFailedCount > 0) {
|
|
114
|
+
(0, telemetry_1.emitFailedRunTelemetry)();
|
|
115
|
+
}
|
|
116
|
+
await (0, telemetry_1.flushTelemetry)();
|
|
117
|
+
if (effectiveFailedCount === 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
61
120
|
if (hasWorkspaceToken && !hasCiEnv && !localUploadEnabled) {
|
|
62
121
|
console.log([
|
|
63
122
|
"Sentinel: Upload skipped.",
|
|
@@ -74,12 +133,18 @@ class SentinelReporter {
|
|
|
74
133
|
console.log(green("✔ Artifacts collected"));
|
|
75
134
|
}
|
|
76
135
|
console.log("");
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
136
|
+
if (hasWorkspaceToken) {
|
|
137
|
+
console.log("Uploading hosted debugging report to Sentinel...");
|
|
138
|
+
}
|
|
139
|
+
else if (usingImplicitLocalPublicMode) {
|
|
140
|
+
console.log("Creating free hosted debug report with Sentinel...");
|
|
141
|
+
console.log(dim("No API key detected. Using the free public report flow for this local run."));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log("Creating free hosted debug report with Sentinel...");
|
|
80
145
|
}
|
|
81
146
|
console.log("");
|
|
82
|
-
const upload = await (0, node_1.runSentinelUpload)({
|
|
147
|
+
const upload = (await (0, node_1.runSentinelUpload)({
|
|
83
148
|
playwrightJsonPath: this.options.playwrightJsonPath,
|
|
84
149
|
playwrightReportDir: this.options.playwrightReportDir,
|
|
85
150
|
testResultsDir: this.options.testResultsDir,
|
|
@@ -90,20 +155,46 @@ class SentinelReporter {
|
|
|
90
155
|
SENTINEL_REPORTER_SILENT: "1",
|
|
91
156
|
SENTINEL_UPLOAD_LOCAL: usingImplicitLocalPublicMode ? "1" : process.env.SENTINEL_UPLOAD_LOCAL
|
|
92
157
|
}
|
|
93
|
-
});
|
|
158
|
+
}));
|
|
94
159
|
if (upload.exitCode !== 0) {
|
|
95
160
|
throw new Error(`Sentinel upload failed with exit code ${upload.exitCode}`);
|
|
96
161
|
}
|
|
162
|
+
const backendReady = upload.diagnosis?.status === "ready" && Boolean(upload.diagnosis?.lines?.length);
|
|
163
|
+
const diagnosis = backendReady ? upload.diagnosis : quickDiagnosis;
|
|
164
|
+
if (diagnosis?.lines.length) {
|
|
165
|
+
console.log("");
|
|
166
|
+
console.log(yellow("Sentinel diagnosis"));
|
|
167
|
+
for (const line of diagnosis.lines) {
|
|
168
|
+
console.log(` ${dim(line)}`);
|
|
169
|
+
}
|
|
170
|
+
if (diagnosis.footer?.length) {
|
|
171
|
+
console.log("");
|
|
172
|
+
for (const line of diagnosis.footer) {
|
|
173
|
+
console.log(` ${dim(line)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const historyLines = failedRunHistory?.lines || [];
|
|
178
|
+
const normalizedHistory = backendReady
|
|
179
|
+
? historyLines.filter((line) => !line.includes("Seen before:"))
|
|
180
|
+
: historyLines;
|
|
181
|
+
if (normalizedHistory.length) {
|
|
182
|
+
console.log("");
|
|
183
|
+
console.log(yellow("Run context"));
|
|
184
|
+
for (const line of normalizedHistory) {
|
|
185
|
+
console.log(` ${dim(line)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
97
188
|
console.log("");
|
|
98
|
-
console.log("
|
|
189
|
+
console.log("View full debug report");
|
|
99
190
|
console.log(` ${upload.shareRunUrl || upload.internalRunUrl}`);
|
|
100
191
|
if (upload.shareLabel) {
|
|
101
192
|
console.log(` ${dim(upload.shareLabel)}`);
|
|
102
193
|
}
|
|
103
194
|
if (!hasWorkspaceToken) {
|
|
104
195
|
console.log("");
|
|
105
|
-
console.log("
|
|
106
|
-
console.log(` ${dim("https://
|
|
196
|
+
console.log("Create a free workspace to keep reports private, compare runs, and unlock deeper AI debugging");
|
|
197
|
+
console.log(` ${dim("https://sentinelqa.com/register")}`);
|
|
107
198
|
}
|
|
108
199
|
}
|
|
109
200
|
}
|
package/dist/runHistory.d.ts
CHANGED
|
@@ -3,5 +3,9 @@ type RunDiffSummary = {
|
|
|
3
3
|
fixedTests: number;
|
|
4
4
|
stillFailing: number;
|
|
5
5
|
};
|
|
6
|
+
export type FailedRunHistorySummary = {
|
|
7
|
+
lines: string[];
|
|
8
|
+
};
|
|
6
9
|
export declare const buildRunDiffSummary: (playwrightJsonPath: string) => RunDiffSummary | null;
|
|
10
|
+
export declare const buildFailedRunHistorySummary: (playwrightJsonPath: string) => FailedRunHistorySummary | null;
|
|
7
11
|
export {};
|
package/dist/runHistory.js
CHANGED
|
@@ -3,11 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.buildRunDiffSummary = void 0;
|
|
6
|
+
exports.buildFailedRunHistorySummary = exports.buildRunDiffSummary = void 0;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const quickDiagnosis_1 = require("./quickDiagnosis");
|
|
10
10
|
const SENTINEL_HISTORY_DIR = node_path_1.default.join(".sentinel", "history");
|
|
11
|
+
const REPORTER_HISTORY_DIR = node_path_1.default.join(".sentinel", "reporter-history");
|
|
11
12
|
const ensureDir = (dirPath) => {
|
|
12
13
|
if (!node_fs_1.default.existsSync(dirPath)) {
|
|
13
14
|
node_fs_1.default.mkdirSync(dirPath, { recursive: true });
|
|
@@ -76,6 +77,16 @@ const normalizeFailures = (snapshot) => {
|
|
|
76
77
|
}
|
|
77
78
|
return [];
|
|
78
79
|
};
|
|
80
|
+
const readJson = (filePath) => {
|
|
81
|
+
if (!node_fs_1.default.existsSync(filePath))
|
|
82
|
+
return null;
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(node_fs_1.default.readFileSync(filePath, "utf8"));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
79
90
|
const readSnapshot = (filePath) => {
|
|
80
91
|
if (!node_fs_1.default.existsSync(filePath))
|
|
81
92
|
return null;
|
|
@@ -86,6 +97,58 @@ const readSnapshot = (filePath) => {
|
|
|
86
97
|
return null;
|
|
87
98
|
}
|
|
88
99
|
};
|
|
100
|
+
const listSnapshots = (branch) => {
|
|
101
|
+
const historyDir = node_path_1.default.resolve(process.cwd(), SENTINEL_HISTORY_DIR);
|
|
102
|
+
if (!node_fs_1.default.existsSync(historyDir))
|
|
103
|
+
return [];
|
|
104
|
+
return node_fs_1.default
|
|
105
|
+
.readdirSync(historyDir)
|
|
106
|
+
.filter((file) => file.endsWith(".json"))
|
|
107
|
+
.map((file) => readSnapshot(node_path_1.default.join(historyDir, file)))
|
|
108
|
+
.filter((value) => Boolean(value))
|
|
109
|
+
.map((snapshot) => ({
|
|
110
|
+
generatedAt: typeof snapshot.generatedAt === "string" ? snapshot.generatedAt : new Date(0).toISOString(),
|
|
111
|
+
branch: typeof snapshot.branch === "string" ? snapshot.branch : branch,
|
|
112
|
+
gitSha: typeof snapshot.gitSha === "string" ? snapshot.gitSha : "unknown",
|
|
113
|
+
failures: normalizeFailures(snapshot)
|
|
114
|
+
}))
|
|
115
|
+
.filter((snapshot) => snapshot.branch === branch)
|
|
116
|
+
.sort((a, b) => b.generatedAt.localeCompare(a.generatedAt));
|
|
117
|
+
};
|
|
118
|
+
const isMeaningfulReporterSnapshot = (snapshot) => {
|
|
119
|
+
if (!snapshot)
|
|
120
|
+
return false;
|
|
121
|
+
if (snapshot.totalTests <= 0)
|
|
122
|
+
return false;
|
|
123
|
+
if ((snapshot.passedCount || 0) === 0 && (snapshot.failedCount || 0) === 0)
|
|
124
|
+
return false;
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
const listReporterSnapshots = (branch) => {
|
|
128
|
+
const historyDir = node_path_1.default.resolve(process.cwd(), REPORTER_HISTORY_DIR);
|
|
129
|
+
if (!node_fs_1.default.existsSync(historyDir))
|
|
130
|
+
return [];
|
|
131
|
+
return node_fs_1.default
|
|
132
|
+
.readdirSync(historyDir)
|
|
133
|
+
.filter((file) => file.endsWith(".json"))
|
|
134
|
+
.map((file) => readJson(node_path_1.default.join(historyDir, file)))
|
|
135
|
+
.filter((value) => Boolean(value))
|
|
136
|
+
.filter((snapshot) => snapshot.branch === branch)
|
|
137
|
+
.filter((snapshot) => isMeaningfulReporterSnapshot(snapshot))
|
|
138
|
+
.sort((a, b) => b.generatedAt.localeCompare(a.generatedAt));
|
|
139
|
+
};
|
|
140
|
+
const normalizeReporterFailures = (snapshot) => {
|
|
141
|
+
if (!snapshot)
|
|
142
|
+
return [];
|
|
143
|
+
return (snapshot.tests || [])
|
|
144
|
+
.filter((test) => test && test.status === "failed")
|
|
145
|
+
.map((test) => ({
|
|
146
|
+
id: typeof test.title === "string" ? test.title : test.matchKey,
|
|
147
|
+
matchKey: typeof test.matchKey === "string" ? test.matchKey : (typeof test.title === "string" ? test.title : "unknown"),
|
|
148
|
+
title: typeof test.title === "string" ? test.title : test.matchKey,
|
|
149
|
+
status: "failed"
|
|
150
|
+
}));
|
|
151
|
+
};
|
|
89
152
|
const writeSnapshot = (snapshot) => {
|
|
90
153
|
ensureDir(node_path_1.default.resolve(process.cwd(), ".sentinel"));
|
|
91
154
|
ensureDir(node_path_1.default.resolve(process.cwd(), SENTINEL_HISTORY_DIR));
|
|
@@ -96,6 +159,7 @@ const writeSnapshot = (snapshot) => {
|
|
|
96
159
|
node_fs_1.default.writeFileSync(node_path_1.default.resolve(process.cwd(), pointerPath), JSON.stringify({ path: historyPath, ...snapshot }, null, 2), "utf8");
|
|
97
160
|
}
|
|
98
161
|
};
|
|
162
|
+
const matchesFailure = (left, right) => left.id === right.id || left.matchKey === right.matchKey;
|
|
99
163
|
const buildRunDiffSummary = (playwrightJsonPath) => {
|
|
100
164
|
const snapshot = buildSnapshot(playwrightJsonPath);
|
|
101
165
|
const previous = readSnapshot(node_path_1.default.resolve(process.cwd(), ".sentinel", `latest-${snapshot.branch}.json`)) ||
|
|
@@ -105,12 +169,58 @@ const buildRunDiffSummary = (playwrightJsonPath) => {
|
|
|
105
169
|
const currentFailureMatchKeys = new Set(snapshot.failures.map((test) => test.matchKey));
|
|
106
170
|
const diff = previous && previous.generatedAt !== snapshot.generatedAt
|
|
107
171
|
? {
|
|
108
|
-
newFailures: snapshot.failures.filter((test) => !previousFailures.some((prev) => prev
|
|
172
|
+
newFailures: snapshot.failures.filter((test) => !previousFailures.some((prev) => matchesFailure(prev, test))).length,
|
|
109
173
|
fixedTests: previousFailures.filter((test) => !currentFailureIds.has(test.id) && !currentFailureMatchKeys.has(test.matchKey)).length,
|
|
110
|
-
stillFailing: snapshot.failures.filter((test) => previousFailures.some((prev) => prev
|
|
174
|
+
stillFailing: snapshot.failures.filter((test) => previousFailures.some((prev) => matchesFailure(prev, test))).length
|
|
111
175
|
}
|
|
112
176
|
: null;
|
|
113
177
|
writeSnapshot(snapshot);
|
|
114
178
|
return diff;
|
|
115
179
|
};
|
|
116
180
|
exports.buildRunDiffSummary = buildRunDiffSummary;
|
|
181
|
+
const buildFailedRunHistorySummary = (playwrightJsonPath) => {
|
|
182
|
+
const snapshot = buildSnapshot(playwrightJsonPath);
|
|
183
|
+
if (snapshot.failures.length === 0)
|
|
184
|
+
return null;
|
|
185
|
+
const reporterSnapshots = listReporterSnapshots(snapshot.branch);
|
|
186
|
+
const previousRun = reporterSnapshots[0] || null;
|
|
187
|
+
const previousFailures = normalizeReporterFailures(previousRun);
|
|
188
|
+
let passStreakBeforeFailure = 0;
|
|
189
|
+
for (const previous of reporterSnapshots) {
|
|
190
|
+
if ((previous.failedCount || 0) === 0) {
|
|
191
|
+
passStreakBeforeFailure += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
const newFailures = snapshot.failures.filter((failure) => !previousFailures.some((prev) => matchesFailure(prev, failure))).length;
|
|
197
|
+
const fixedTests = previousFailures.filter((failure) => !snapshot.failures.some((current) => matchesFailure(current, failure))).length;
|
|
198
|
+
const stillFailing = snapshot.failures.filter((failure) => previousFailures.some((prev) => matchesFailure(prev, failure))).length;
|
|
199
|
+
const failingReporterRuns = reporterSnapshots.filter((run) => (run.failedCount || 0) > 0);
|
|
200
|
+
const recurringFailures = snapshot.failures
|
|
201
|
+
.map((failure) => ({
|
|
202
|
+
failure,
|
|
203
|
+
occurrences: failingReporterRuns.filter((run) => normalizeReporterFailures(run).some((prev) => matchesFailure(prev, failure))).length
|
|
204
|
+
}))
|
|
205
|
+
.sort((a, b) => b.occurrences - a.occurrences);
|
|
206
|
+
const topRecurring = recurringFailures.find((item) => item.occurrences > 0) || null;
|
|
207
|
+
writeSnapshot(snapshot);
|
|
208
|
+
const lines = [];
|
|
209
|
+
if (passStreakBeforeFailure > 0) {
|
|
210
|
+
lines.push(`- First failure after ${passStreakBeforeFailure} passing runs`);
|
|
211
|
+
}
|
|
212
|
+
if (previousRun && (newFailures > 0 || fixedTests > 0 || stillFailing > 0)) {
|
|
213
|
+
const previousWasGreen = (previousRun.failedCount || 0) === 0;
|
|
214
|
+
if (previousWasGreen) {
|
|
215
|
+
lines.push(`- The immediately previous run was green. Compared to that previous run: ${newFailures} newly failing in this run, ${stillFailing} still failing, ${fixedTests} no longer failing`);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
lines.push(`- Compared to the immediately previous run: ${newFailures} newly failing in this run, ${stillFailing} still failing, ${fixedTests} no longer failing`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (topRecurring) {
|
|
222
|
+
lines.push(`- Recurring across ${topRecurring.occurrences + 1} recorded failed runs in local history (${topRecurring.failure.title})`);
|
|
223
|
+
}
|
|
224
|
+
return lines.length ? { lines } : null;
|
|
225
|
+
};
|
|
226
|
+
exports.buildFailedRunHistorySummary = buildFailedRunHistorySummary;
|
|
@@ -0,0 +1,173 @@
|
|
|
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.flushTelemetry = exports.emitFailedRunTelemetry = exports.emitReporterTelemetry = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const http_1 = __importDefault(require("http"));
|
|
9
|
+
const https_1 = __importDefault(require("https"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const DEFAULT_APP_URL = "https://app.sentinelqa.com";
|
|
12
|
+
const PACKAGE_VERSION = (() => {
|
|
13
|
+
try {
|
|
14
|
+
return require("../package.json").version || "0.0.0";
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return "0.0.0";
|
|
18
|
+
}
|
|
19
|
+
})();
|
|
20
|
+
const globalState = globalThis;
|
|
21
|
+
const readEnv = (key) => {
|
|
22
|
+
const value = process.env[key];
|
|
23
|
+
return value && value.trim().length > 0 ? value.trim() : null;
|
|
24
|
+
};
|
|
25
|
+
const isTruthyEnv = (key) => {
|
|
26
|
+
const value = readEnv(key);
|
|
27
|
+
if (!value)
|
|
28
|
+
return false;
|
|
29
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
30
|
+
};
|
|
31
|
+
const debugLog = (message) => {
|
|
32
|
+
if (!isTruthyEnv("SENTINEL_TELEMETRY_DEBUG"))
|
|
33
|
+
return;
|
|
34
|
+
console.log(`[sentinel-telemetry] ${message}`);
|
|
35
|
+
};
|
|
36
|
+
const gitOutput = (args) => {
|
|
37
|
+
try {
|
|
38
|
+
return (0, child_process_1.execFileSync)("git", args, {
|
|
39
|
+
cwd: process.cwd(),
|
|
40
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
41
|
+
encoding: "utf8"
|
|
42
|
+
}).trim();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const getCiProvider = () => {
|
|
49
|
+
if (readEnv("GITHUB_ACTIONS") === "true")
|
|
50
|
+
return "github";
|
|
51
|
+
if (readEnv("GITLAB_CI") === "true" || readEnv("CI_PROJECT_ID"))
|
|
52
|
+
return "gitlab";
|
|
53
|
+
if (readEnv("CIRCLECI") === "true")
|
|
54
|
+
return "circleci";
|
|
55
|
+
return "local";
|
|
56
|
+
};
|
|
57
|
+
const detectRepoIdentity = (provider) => {
|
|
58
|
+
if (provider === "github") {
|
|
59
|
+
const repo = readEnv("GITHUB_REPOSITORY");
|
|
60
|
+
if (repo)
|
|
61
|
+
return repo.toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
if (provider === "gitlab") {
|
|
64
|
+
const repo = readEnv("CI_PROJECT_PATH") || readEnv("CI_PROJECT_URL");
|
|
65
|
+
if (repo)
|
|
66
|
+
return repo.toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
if (provider === "circleci") {
|
|
69
|
+
const user = readEnv("CIRCLE_PROJECT_USERNAME");
|
|
70
|
+
const repo = readEnv("CIRCLE_PROJECT_REPONAME");
|
|
71
|
+
if (user && repo)
|
|
72
|
+
return `${user}/${repo}`.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
const remote = gitOutput(["config", "--get", "remote.origin.url"]) ||
|
|
75
|
+
gitOutput(["remote", "get-url", "origin"]);
|
|
76
|
+
if (remote)
|
|
77
|
+
return remote.toLowerCase();
|
|
78
|
+
return process.cwd().toLowerCase();
|
|
79
|
+
};
|
|
80
|
+
const buildPayload = (eventType) => {
|
|
81
|
+
const ciProvider = getCiProvider();
|
|
82
|
+
const repoHash = crypto_1.default
|
|
83
|
+
.createHash("sha256")
|
|
84
|
+
.update(detectRepoIdentity(ciProvider))
|
|
85
|
+
.digest("hex");
|
|
86
|
+
return {
|
|
87
|
+
repoHash,
|
|
88
|
+
eventType,
|
|
89
|
+
environment: ciProvider === "local" ? "local" : "ci",
|
|
90
|
+
ciProvider,
|
|
91
|
+
mode: readEnv("SENTINEL_TOKEN") ? "workspace" : "public",
|
|
92
|
+
version: PACKAGE_VERSION
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
const getPendingTelemetry = () => {
|
|
96
|
+
if (!globalState.__sentinelTelemetryPending) {
|
|
97
|
+
globalState.__sentinelTelemetryPending = [];
|
|
98
|
+
}
|
|
99
|
+
return globalState.__sentinelTelemetryPending;
|
|
100
|
+
};
|
|
101
|
+
const postJson = (url, payload) => new Promise((resolve) => {
|
|
102
|
+
const target = new URL(url);
|
|
103
|
+
const client = target.protocol === "https:" ? https_1.default : http_1.default;
|
|
104
|
+
const body = JSON.stringify(payload);
|
|
105
|
+
const req = client.request({
|
|
106
|
+
method: "POST",
|
|
107
|
+
hostname: target.hostname,
|
|
108
|
+
port: target.port,
|
|
109
|
+
path: `${target.pathname}${target.search}`,
|
|
110
|
+
headers: {
|
|
111
|
+
"content-type": "application/json",
|
|
112
|
+
"content-length": Buffer.byteLength(body).toString()
|
|
113
|
+
}
|
|
114
|
+
}, (res) => {
|
|
115
|
+
debugLog(`POST ${target.origin}${target.pathname} -> ${res.statusCode || 0}`);
|
|
116
|
+
res.resume();
|
|
117
|
+
res.on("end", () => resolve());
|
|
118
|
+
});
|
|
119
|
+
req.setTimeout(1200, () => {
|
|
120
|
+
debugLog(`timeout posting to ${target.origin}${target.pathname}`);
|
|
121
|
+
req.destroy();
|
|
122
|
+
resolve();
|
|
123
|
+
});
|
|
124
|
+
req.on("error", (error) => {
|
|
125
|
+
debugLog(`error posting to ${target.origin}${target.pathname}: ${error.message}`);
|
|
126
|
+
resolve();
|
|
127
|
+
});
|
|
128
|
+
req.write(body);
|
|
129
|
+
req.end();
|
|
130
|
+
});
|
|
131
|
+
const emitTelemetry = (eventType) => {
|
|
132
|
+
if (isTruthyEnv("SENTINEL_TELEMETRY_DISABLED"))
|
|
133
|
+
return;
|
|
134
|
+
const appUrl = readEnv("SENTINEL_APP_URL") || DEFAULT_APP_URL;
|
|
135
|
+
const payload = buildPayload(eventType);
|
|
136
|
+
debugLog(`event=${eventType} dest=${appUrl}/api/telemetry/uploader env=${payload.environment} provider=${payload.ciProvider} mode=${payload.mode} version=${payload.version} repo=${payload.repoHash.slice(0, 12)}`);
|
|
137
|
+
try {
|
|
138
|
+
const pending = getPendingTelemetry();
|
|
139
|
+
const request = postJson(`${appUrl}/api/telemetry/uploader`, payload).finally(() => {
|
|
140
|
+
const index = pending.indexOf(request);
|
|
141
|
+
if (index !== -1)
|
|
142
|
+
pending.splice(index, 1);
|
|
143
|
+
});
|
|
144
|
+
pending.push(request);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Telemetry must never affect test execution.
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const emitReporterTelemetry = () => {
|
|
151
|
+
if (globalState.__sentinelReporterTelemetrySent)
|
|
152
|
+
return;
|
|
153
|
+
globalState.__sentinelReporterTelemetrySent = true;
|
|
154
|
+
emitTelemetry("seen");
|
|
155
|
+
};
|
|
156
|
+
exports.emitReporterTelemetry = emitReporterTelemetry;
|
|
157
|
+
const emitFailedRunTelemetry = () => {
|
|
158
|
+
if (globalState.__sentinelReporterFailedTelemetrySent)
|
|
159
|
+
return;
|
|
160
|
+
globalState.__sentinelReporterFailedTelemetrySent = true;
|
|
161
|
+
emitTelemetry("failed_run");
|
|
162
|
+
};
|
|
163
|
+
exports.emitFailedRunTelemetry = emitFailedRunTelemetry;
|
|
164
|
+
const flushTelemetry = async (timeoutMs = 1500) => {
|
|
165
|
+
const pending = [...getPendingTelemetry()];
|
|
166
|
+
if (pending.length === 0)
|
|
167
|
+
return;
|
|
168
|
+
await Promise.race([
|
|
169
|
+
Promise.allSettled(pending).then(() => undefined),
|
|
170
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs))
|
|
171
|
+
]);
|
|
172
|
+
};
|
|
173
|
+
exports.flushTelemetry = flushTelemetry;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type HistoryTest = {
|
|
2
|
+
matchKey: string;
|
|
3
|
+
title: string;
|
|
4
|
+
status: string;
|
|
5
|
+
durationMs: number;
|
|
6
|
+
retries: number;
|
|
7
|
+
timeoutMs: number | null;
|
|
8
|
+
};
|
|
9
|
+
type HistorySnapshot = {
|
|
10
|
+
generatedAt: string;
|
|
11
|
+
branch: string;
|
|
12
|
+
gitSha: string;
|
|
13
|
+
wallDurationMs: number | null;
|
|
14
|
+
totalTests: number;
|
|
15
|
+
passedCount: number;
|
|
16
|
+
failedCount: number;
|
|
17
|
+
skippedCount: number;
|
|
18
|
+
retryPassedCount: number;
|
|
19
|
+
failures: string[];
|
|
20
|
+
tests: HistoryTest[];
|
|
21
|
+
};
|
|
22
|
+
export type PassingRunSummary = {
|
|
23
|
+
lines: string[];
|
|
24
|
+
risks: string[];
|
|
25
|
+
};
|
|
26
|
+
type PassingRunSummaryOptions = {
|
|
27
|
+
observedRunDurationMs?: number | null;
|
|
28
|
+
};
|
|
29
|
+
export declare const recordReporterHistorySnapshot: (playwrightJsonPath: string) => HistorySnapshot;
|
|
30
|
+
export declare const buildPassingRunSummary: (playwrightJsonPath: string, options?: PassingRunSummaryOptions) => PassingRunSummary | null;
|
|
31
|
+
export {};
|