@witchpot/gmc 0.1.7 → 0.1.9
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/CHANGELOG.md +16 -0
- package/README.md +17 -12
- package/package.json +3 -1
- package/scripts/smoke-beta.mjs +35 -9
- package/skill/gmc-analysis/CHANGELOG.md +15 -0
- package/skill/gmc-analysis/SKILL.md +11 -3
- package/skill/gmc-analysis/references/pitfalls.md +26 -2
- package/skill/gmc-analysis/references/quota.md +7 -1
- package/skill/gmc-analysis/references/recipes.md +16 -1
- package/src/args.js +1 -1
- package/src/config.js +5 -5
- package/src/constants.js +1 -1
- package/src/help.js +4 -2
- package/src/main.js +12 -0
- package/src/requests.js +17 -5
- package/src/requests.test.js +43 -6
- package/src/skill.js +2 -1
- package/src/telemetry.js +341 -0
- package/src/telemetry.test.js +121 -0
- package/src/workflows.js +81 -27
- package/src/workflows.test.js +28 -1
package/src/telemetry.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { CLI_VERSION } from "./constants.js";
|
|
2
|
+
import { buildRequestHeaders } from "./requests.js";
|
|
3
|
+
|
|
4
|
+
const TELEMETRY_TIMEOUT_MS = 800;
|
|
5
|
+
const TELEMETRY_ENDPOINT = "/api/v1/product-events";
|
|
6
|
+
const FORMAT_OPTION_KEYS = new Set(["json", "fields", "preset", "help", "version", "format"]);
|
|
7
|
+
const RAW_TEXT_KEYS = new Set(["q", "query", "title", "topic", "message", "prompt"]);
|
|
8
|
+
const SENSITIVE_OPTION_KEYS = new Set([
|
|
9
|
+
"api-key",
|
|
10
|
+
"authorization",
|
|
11
|
+
"bearer",
|
|
12
|
+
"credential",
|
|
13
|
+
"key",
|
|
14
|
+
"secret",
|
|
15
|
+
"token"
|
|
16
|
+
]);
|
|
17
|
+
const SAFE_QUERY_VALUE_KEYS = new Set([
|
|
18
|
+
"bucket",
|
|
19
|
+
"compareBy",
|
|
20
|
+
"coverage",
|
|
21
|
+
"detail",
|
|
22
|
+
"format",
|
|
23
|
+
"focus",
|
|
24
|
+
"groupBy",
|
|
25
|
+
"includeTotal",
|
|
26
|
+
"limit",
|
|
27
|
+
"locale",
|
|
28
|
+
"maxTopicsPerGame",
|
|
29
|
+
"metric",
|
|
30
|
+
"minGames",
|
|
31
|
+
"offset",
|
|
32
|
+
"priceMax",
|
|
33
|
+
"priceMin",
|
|
34
|
+
"ratingMin",
|
|
35
|
+
"releaseFrom",
|
|
36
|
+
"releaseTo",
|
|
37
|
+
"reviewsGt",
|
|
38
|
+
"reviewsLt",
|
|
39
|
+
"reviewsMax",
|
|
40
|
+
"reviewsMin",
|
|
41
|
+
"samplePages",
|
|
42
|
+
"sampleSize",
|
|
43
|
+
"sections",
|
|
44
|
+
"sentiment",
|
|
45
|
+
"sort",
|
|
46
|
+
"source",
|
|
47
|
+
"status",
|
|
48
|
+
"tagMode",
|
|
49
|
+
"tags"
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
export async function trackCliCommandExecuted({
|
|
53
|
+
baseUrl,
|
|
54
|
+
apiKey,
|
|
55
|
+
command,
|
|
56
|
+
options,
|
|
57
|
+
positional,
|
|
58
|
+
request,
|
|
59
|
+
response,
|
|
60
|
+
startedAt
|
|
61
|
+
}) {
|
|
62
|
+
if (!apiKey || isCliTelemetryDisabled()) return;
|
|
63
|
+
|
|
64
|
+
const event = buildCliCommandTelemetryEvent({
|
|
65
|
+
command,
|
|
66
|
+
options,
|
|
67
|
+
positional,
|
|
68
|
+
request,
|
|
69
|
+
response,
|
|
70
|
+
startedAt
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await sendCliTelemetryEvent(baseUrl, apiKey, event);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildCliCommandTelemetryEvent({
|
|
77
|
+
command,
|
|
78
|
+
options = {},
|
|
79
|
+
positional = [],
|
|
80
|
+
request,
|
|
81
|
+
response,
|
|
82
|
+
startedAt,
|
|
83
|
+
now = Date.now()
|
|
84
|
+
}) {
|
|
85
|
+
const appid = extractSingleAppid(request);
|
|
86
|
+
const queryInputs = collectRequestQueries(request);
|
|
87
|
+
const safeQueryValues = mergeSafeQueryValues(queryInputs);
|
|
88
|
+
const properties = compactObject({
|
|
89
|
+
source: "gmc-cli",
|
|
90
|
+
cliVersion: CLI_VERSION,
|
|
91
|
+
command: command.join(" "),
|
|
92
|
+
commandGroup: command[0],
|
|
93
|
+
commandAction: command.slice(1).join(" ") || undefined,
|
|
94
|
+
workflow: request.workflow ?? request.kind ?? "direct",
|
|
95
|
+
endpointTemplate: getPrimaryEndpointTemplate(request),
|
|
96
|
+
success: true,
|
|
97
|
+
durationMs: boundedInteger(now - startedAt),
|
|
98
|
+
outputMode: options.json ? "json" : "default",
|
|
99
|
+
optionKeys: safeOptionKeys(options),
|
|
100
|
+
filterKeys: safeFilterKeys(queryInputs),
|
|
101
|
+
hasSearchText: hasRawTextInput(options, request, positional),
|
|
102
|
+
externalIdCount: countExternalIds(request),
|
|
103
|
+
tags: normalizeTokenList(safeQueryValues.tags),
|
|
104
|
+
sections: normalizeTokenList(safeQueryValues.sections),
|
|
105
|
+
coverage: normalizeTokenList(safeQueryValues.coverage),
|
|
106
|
+
focus: safeToken(request.focus ?? safeQueryValues.focus),
|
|
107
|
+
compareBy: safeToken(request.compareBy ?? safeQueryValues.compareBy),
|
|
108
|
+
evidenceType: safeToken(request.evidenceType),
|
|
109
|
+
metric: safeToken(safeQueryValues.metric),
|
|
110
|
+
groupBy: safeToken(safeQueryValues.groupBy),
|
|
111
|
+
bucket: safeToken(safeQueryValues.bucket),
|
|
112
|
+
sort: safeToken(safeQueryValues.sort),
|
|
113
|
+
status: safeToken(safeQueryValues.status),
|
|
114
|
+
format: safeToken(safeQueryValues.format),
|
|
115
|
+
tagMode: safeToken(safeQueryValues.tagMode),
|
|
116
|
+
limit: safeNumber(safeQueryValues.limit ?? request.limit),
|
|
117
|
+
offset: safeNumber(safeQueryValues.offset),
|
|
118
|
+
minGames: safeNumber(request.minGames),
|
|
119
|
+
maxTopicsPerGame: safeNumber(request.maxTopicsPerGame),
|
|
120
|
+
sampleMode: safeToken(request.sampling?.mode),
|
|
121
|
+
sampleRequestedSize: safeNumber(request.sampling?.requestedSize),
|
|
122
|
+
sampleRequestedPages: safeNumber(request.sampling?.requestedPages),
|
|
123
|
+
samplePageLimit: safeNumber(request.sampling?.pageLimit),
|
|
124
|
+
includeTotal: Boolean(safeQueryValues.includeTotal || request.countRequest),
|
|
125
|
+
releaseFrom: safeDate(safeQueryValues.releaseFrom),
|
|
126
|
+
releaseTo: safeDate(safeQueryValues.releaseTo),
|
|
127
|
+
reviewsMin: safeNumber(safeQueryValues.reviewsMin),
|
|
128
|
+
reviewsMax: safeNumber(safeQueryValues.reviewsMax),
|
|
129
|
+
reviewsGt: safeNumber(safeQueryValues.reviewsGt),
|
|
130
|
+
reviewsLt: safeNumber(safeQueryValues.reviewsLt),
|
|
131
|
+
ratingMin: safeNumber(safeQueryValues.ratingMin),
|
|
132
|
+
priceMin: safeNumber(safeQueryValues.priceMin),
|
|
133
|
+
priceMax: safeNumber(safeQueryValues.priceMax),
|
|
134
|
+
rowCount: topLevelRowCount(response),
|
|
135
|
+
warningCount: safeWarnings(response).length,
|
|
136
|
+
lockedCount: safeLockedSections(response).length,
|
|
137
|
+
resultKind: safeToken(response?.data?.kind),
|
|
138
|
+
apiRequestId: safeShortString(response?.meta?.requestId)
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return compactObject({
|
|
142
|
+
eventType: "cli_command_executed",
|
|
143
|
+
appid,
|
|
144
|
+
properties
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function sendCliTelemetryEvent(baseUrl, apiKey, event, {
|
|
149
|
+
fetchImpl = globalThis.fetch,
|
|
150
|
+
timeoutMs = TELEMETRY_TIMEOUT_MS
|
|
151
|
+
} = {}) {
|
|
152
|
+
if (typeof fetchImpl !== "function") return;
|
|
153
|
+
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const url = new URL(TELEMETRY_ENDPOINT, baseUrl);
|
|
159
|
+
await fetchImpl(url, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: {
|
|
162
|
+
...buildRequestHeaders(apiKey),
|
|
163
|
+
"content-type": "application/json"
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify(event),
|
|
166
|
+
signal: controller.signal
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
// CLI telemetry is best-effort and must never change command behavior.
|
|
170
|
+
} finally {
|
|
171
|
+
clearTimeout(timeout);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function isCliTelemetryDisabled(env = process.env) {
|
|
176
|
+
if (env.GMC_CLI_TELEMETRY != null) {
|
|
177
|
+
return /^(0|false|off|no|disabled)$/i.test(String(env.GMC_CLI_TELEMETRY));
|
|
178
|
+
}
|
|
179
|
+
if (env.GMC_DISABLE_TELEMETRY != null) {
|
|
180
|
+
return /^(1|true|on|yes|enabled)$/i.test(String(env.GMC_DISABLE_TELEMETRY));
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function safeOptionKeys(options) {
|
|
186
|
+
return Object.keys(options)
|
|
187
|
+
.filter((key) => !FORMAT_OPTION_KEYS.has(key) && !SENSITIVE_OPTION_KEYS.has(key))
|
|
188
|
+
.sort()
|
|
189
|
+
.slice(0, 40);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function collectRequestQueries(request) {
|
|
193
|
+
return [
|
|
194
|
+
request.query,
|
|
195
|
+
request.searchRequest?.query,
|
|
196
|
+
request.countRequest?.query,
|
|
197
|
+
request.topicQuery,
|
|
198
|
+
request.detailQuery,
|
|
199
|
+
request.analysisQuery,
|
|
200
|
+
request.showcaseQuery
|
|
201
|
+
].filter((query) => query && typeof query === "object");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function mergeSafeQueryValues(queries) {
|
|
205
|
+
const values = {};
|
|
206
|
+
for (const query of queries) {
|
|
207
|
+
for (const [key, value] of Object.entries(query)) {
|
|
208
|
+
if (SAFE_QUERY_VALUE_KEYS.has(key) && value != null && value !== "") values[key] = value;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return values;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function safeFilterKeys(queries) {
|
|
215
|
+
const keys = new Set();
|
|
216
|
+
for (const query of queries) {
|
|
217
|
+
for (const [key, value] of Object.entries(query)) {
|
|
218
|
+
if (value == null || value === "" || RAW_TEXT_KEYS.has(key)) continue;
|
|
219
|
+
keys.add(key);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return [...keys].sort().slice(0, 40);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function hasRawTextInput(options, request, positional) {
|
|
226
|
+
if (Object.keys(options).some((key) => RAW_TEXT_KEYS.has(key) && options[key] != null && options[key] !== "")) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
if (typeof request.query === "string" && request.query.trim()) return true;
|
|
230
|
+
if (typeof request.topic === "string" && request.topic.trim()) return true;
|
|
231
|
+
if (request.query && typeof request.query === "object" && typeof request.query.q === "string" && request.query.q.trim()) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if ((request.kind === "resolve" || request.workflow) && positional.some((value) => String(value).trim())) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getPrimaryEndpointTemplate(request) {
|
|
241
|
+
const path = request.path ?? request.searchRequest?.path ?? request.countRequest?.path;
|
|
242
|
+
if (!path) return undefined;
|
|
243
|
+
return templateEndpoint(path);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function templateEndpoint(path) {
|
|
247
|
+
return String(path)
|
|
248
|
+
.replace(/\/api\/v1\/steam\/games\/[^/]+/g, "/api/v1/steam/games/{externalId}")
|
|
249
|
+
.replace(/\/\d+(?=\/|$)/g, "/{id}");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function extractSingleAppid(request) {
|
|
253
|
+
const candidates = [
|
|
254
|
+
request.externalId,
|
|
255
|
+
...(Array.isArray(request.externalIds) ? request.externalIds : []),
|
|
256
|
+
extractAppidFromPath(request.path)
|
|
257
|
+
].filter(Boolean);
|
|
258
|
+
const unique = [...new Set(candidates.map((value) => String(value)))];
|
|
259
|
+
if (unique.length !== 1 || !/^\d+$/.test(unique[0])) return undefined;
|
|
260
|
+
const appid = Number(unique[0]);
|
|
261
|
+
return Number.isSafeInteger(appid) && appid > 0 ? appid : undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function extractAppidFromPath(path) {
|
|
265
|
+
if (!path) return undefined;
|
|
266
|
+
const match = String(path).match(/\/api\/v1\/steam\/games\/([^/]+)/);
|
|
267
|
+
if (!match) return undefined;
|
|
268
|
+
return decodeURIComponent(match[1]);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function countExternalIds(request) {
|
|
272
|
+
if (Array.isArray(request.externalIds)) return request.externalIds.length;
|
|
273
|
+
if (request.externalId) return 1;
|
|
274
|
+
return extractAppidFromPath(request.path) ? 1 : 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeTokenList(value) {
|
|
278
|
+
const values = Array.isArray(value) ? value : String(value ?? "").split(",");
|
|
279
|
+
const normalized = values
|
|
280
|
+
.map((item) => safeToken(item))
|
|
281
|
+
.filter(Boolean);
|
|
282
|
+
return normalized.length ? [...new Set(normalized)].slice(0, 20) : undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function safeToken(value) {
|
|
286
|
+
if (value == null || value === "") return undefined;
|
|
287
|
+
const normalized = String(value)
|
|
288
|
+
.trim()
|
|
289
|
+
.replace(/[^a-zA-Z0-9_.:-]/g, "_")
|
|
290
|
+
.replace(/_+/g, "_")
|
|
291
|
+
.slice(0, 80);
|
|
292
|
+
return normalized || undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function safeShortString(value) {
|
|
296
|
+
if (typeof value !== "string") return undefined;
|
|
297
|
+
const trimmed = value.trim();
|
|
298
|
+
return trimmed ? trimmed.slice(0, 120) : undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function safeDate(value) {
|
|
302
|
+
if (typeof value !== "string") return undefined;
|
|
303
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(value) ? value : undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function safeNumber(value) {
|
|
307
|
+
if (value == null || value === "") return undefined;
|
|
308
|
+
const number = Number(value);
|
|
309
|
+
return Number.isFinite(number) ? number : undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function boundedInteger(value) {
|
|
313
|
+
const number = Number(value);
|
|
314
|
+
if (!Number.isFinite(number)) return undefined;
|
|
315
|
+
return Math.max(0, Math.round(number));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function topLevelRowCount(response) {
|
|
319
|
+
if (Array.isArray(response?.data)) return response.data.length;
|
|
320
|
+
if (Array.isArray(response?.data?.games)) return response.data.games.length;
|
|
321
|
+
if (Array.isArray(response?.data?.cohort?.included)) return response.data.cohort.included.length;
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function safeWarnings(response) {
|
|
326
|
+
if (Array.isArray(response?.warnings)) return response.warnings;
|
|
327
|
+
if (Array.isArray(response?.data?.warnings)) return response.data.warnings;
|
|
328
|
+
if (Array.isArray(response?.meta?.warnings)) return response.meta.warnings;
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function safeLockedSections(response) {
|
|
333
|
+
const locked = response?.meta?.entitlements?.locked;
|
|
334
|
+
return Array.isArray(locked) ? locked : [];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function compactObject(value) {
|
|
338
|
+
return Object.fromEntries(
|
|
339
|
+
Object.entries(value).filter(([, entry]) => entry !== undefined)
|
|
340
|
+
);
|
|
341
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildCliCommandTelemetryEvent,
|
|
6
|
+
isCliTelemetryDisabled,
|
|
7
|
+
sendCliTelemetryEvent
|
|
8
|
+
} from "./telemetry.js";
|
|
9
|
+
|
|
10
|
+
test("buildCliCommandTelemetryEvent captures safe CLI filters without raw search text", () => {
|
|
11
|
+
const event = buildCliCommandTelemetryEvent({
|
|
12
|
+
command: ["games", "search"],
|
|
13
|
+
options: {
|
|
14
|
+
source: "steam",
|
|
15
|
+
query: "cozy fishing games like Dredge",
|
|
16
|
+
tags: "cozy,fishing",
|
|
17
|
+
"reviews-min": "100",
|
|
18
|
+
"api-key": "gmc_should_not_appear",
|
|
19
|
+
json: true
|
|
20
|
+
},
|
|
21
|
+
positional: [],
|
|
22
|
+
request: {
|
|
23
|
+
path: "/api/v1/steam/games",
|
|
24
|
+
query: {
|
|
25
|
+
q: "cozy fishing games like Dredge",
|
|
26
|
+
tags: "cozy,fishing",
|
|
27
|
+
reviewsMin: "100",
|
|
28
|
+
source: "steam",
|
|
29
|
+
limit: "20"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
response: {
|
|
33
|
+
data: [{ appid: 1 }, { appid: 2 }],
|
|
34
|
+
meta: {
|
|
35
|
+
requestId: "req_123"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
startedAt: 1_000,
|
|
39
|
+
now: 1_250
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const serialized = JSON.stringify(event);
|
|
43
|
+
assert.equal(event.eventType, "cli_command_executed");
|
|
44
|
+
assert.equal(event.properties.command, "games search");
|
|
45
|
+
assert.deepEqual(event.properties.tags, ["cozy", "fishing"]);
|
|
46
|
+
assert.equal(event.properties.reviewsMin, 100);
|
|
47
|
+
assert.equal(event.properties.hasSearchText, true);
|
|
48
|
+
assert.equal(event.properties.rowCount, 2);
|
|
49
|
+
assert.equal(event.properties.durationMs, 250);
|
|
50
|
+
assert.equal(event.properties.optionKeys.includes("api-key"), false);
|
|
51
|
+
assert.equal(serialized.includes("Dredge"), false);
|
|
52
|
+
assert.equal(serialized.includes("cozy fishing games"), false);
|
|
53
|
+
assert.equal(serialized.includes("gmc_should_not_appear"), false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("buildCliCommandTelemetryEvent templates app endpoints and attaches single appid", () => {
|
|
57
|
+
const event = buildCliCommandTelemetryEvent({
|
|
58
|
+
command: ["games", "analysis"],
|
|
59
|
+
options: {
|
|
60
|
+
source: "steam",
|
|
61
|
+
sections: "reviews,marketing",
|
|
62
|
+
json: true
|
|
63
|
+
},
|
|
64
|
+
positional: ["1384160"],
|
|
65
|
+
request: {
|
|
66
|
+
path: "/api/v1/steam/games/1384160/analysis",
|
|
67
|
+
query: {
|
|
68
|
+
sections: "reviews,marketing"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
response: {
|
|
72
|
+
data: {
|
|
73
|
+
kind: "game_analysis"
|
|
74
|
+
},
|
|
75
|
+
meta: {
|
|
76
|
+
entitlements: {
|
|
77
|
+
locked: ["marketing.full"]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
startedAt: 2_000,
|
|
82
|
+
now: 2_040
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
assert.equal(event.appid, 1384160);
|
|
86
|
+
assert.equal(event.properties.endpointTemplate, "/api/v1/steam/games/{externalId}/analysis");
|
|
87
|
+
assert.deepEqual(event.properties.sections, ["reviews", "marketing"]);
|
|
88
|
+
assert.equal(event.properties.lockedCount, 1);
|
|
89
|
+
assert.equal(event.properties.resultKind, "game_analysis");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("sendCliTelemetryEvent posts product events with bearer auth and swallows failures", async () => {
|
|
93
|
+
const calls = [];
|
|
94
|
+
const fetchImpl = async (url, init) => {
|
|
95
|
+
calls.push({ url, init });
|
|
96
|
+
throw new Error("network unavailable");
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await sendCliTelemetryEvent("https://example.test", "gmcc_secret", {
|
|
100
|
+
eventType: "cli_command_executed",
|
|
101
|
+
properties: {
|
|
102
|
+
source: "gmc-cli"
|
|
103
|
+
}
|
|
104
|
+
}, {
|
|
105
|
+
fetchImpl,
|
|
106
|
+
timeoutMs: 20
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
assert.equal(calls.length, 1);
|
|
110
|
+
assert.equal(String(calls[0].url), "https://example.test/api/v1/product-events");
|
|
111
|
+
assert.equal(calls[0].init.method, "POST");
|
|
112
|
+
assert.equal(calls[0].init.headers.authorization, "Bearer gmcc_secret");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("isCliTelemetryDisabled supports opt-out environment values", () => {
|
|
116
|
+
assert.equal(isCliTelemetryDisabled({}), false);
|
|
117
|
+
assert.equal(isCliTelemetryDisabled({ GMC_CLI_TELEMETRY: "0" }), true);
|
|
118
|
+
assert.equal(isCliTelemetryDisabled({ GMC_CLI_TELEMETRY: "1" }), false);
|
|
119
|
+
assert.equal(isCliTelemetryDisabled({ GMC_DISABLE_TELEMETRY: "1" }), true);
|
|
120
|
+
assert.equal(isCliTelemetryDisabled({ GMC_CLI_TELEMETRY: "true" }), false);
|
|
121
|
+
});
|