@mintlify/cli 4.0.1472 → 4.0.1474

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.
@@ -1,418 +0,0 @@
1
- import fs from 'fs';
2
- import * as fsExtra from 'fs-extra';
3
- import os from 'os';
4
-
5
- import { isTelemetryEnabled, setTelemetryEnabled } from '../src/config.js';
6
- import { TELEMETRY_ASYNC_TIMEOUT_MS } from '../src/constants.js';
7
- import {
8
- createTelemetryMiddleware,
9
- getSanitizedCommandForTelemetry,
10
- } from '../src/middlewares/telemetryMiddleware.js';
11
- import { getDistinctId } from '../src/telemetry/distinctId.js';
12
- import * as trackModule from '../src/telemetry/track.js';
13
- import {
14
- trackCommand,
15
- trackEvent,
16
- trackTelemetryPreferenceChange,
17
- } from '../src/telemetry/track.js';
18
-
19
- vi.mock('fs-extra', () => ({ ensureDir: vi.fn().mockResolvedValue(undefined) }));
20
-
21
- const mockCaptureImmediate = vi.fn().mockResolvedValue(undefined);
22
-
23
- vi.mock('../src/telemetry/client.js', () => ({
24
- getPostHogClient: () => ({ captureImmediate: mockCaptureImmediate }),
25
- shutdownPostHog: vi.fn(),
26
- }));
27
-
28
- afterEach(() => {
29
- vi.restoreAllMocks();
30
- });
31
-
32
- describe('createTelemetryMiddleware', () => {
33
- beforeEach(() => {
34
- vi.spyOn(trackModule, 'trackCommand').mockResolvedValue(undefined);
35
- });
36
-
37
- it('only tracks once when middleware runs multiple times for the same parse', async () => {
38
- const middleware = createTelemetryMiddleware();
39
- await middleware({ _: ['dev'] });
40
- await middleware({ _: ['dev'] });
41
- expect(trackModule.trackCommand).toHaveBeenCalledTimes(1);
42
- });
43
-
44
- it('tracks again for a new middleware instance (new cli invocation)', async () => {
45
- await createTelemetryMiddleware()({ _: ['dev'] });
46
- expect(trackModule.trackCommand).toHaveBeenCalledTimes(1);
47
- await createTelemetryMiddleware()({ _: ['build'] });
48
- expect(trackModule.trackCommand).toHaveBeenCalledTimes(2);
49
- });
50
- });
51
-
52
- describe('getSanitizedCommandForTelemetry', () => {
53
- it('includes known scrape subcommands', () => {
54
- expect(getSanitizedCommandForTelemetry(['scrape', 'page', 'https://example.com'])).toBe(
55
- 'scrape page'
56
- );
57
- expect(getSanitizedCommandForTelemetry(['scrape', 'site', 'https://example.com'])).toBe(
58
- 'scrape site'
59
- );
60
- expect(getSanitizedCommandForTelemetry(['scrape', 'openapi', './spec.yaml'])).toBe(
61
- 'scrape openapi'
62
- );
63
- });
64
-
65
- it('strips positional args from all other commands', () => {
66
- expect(getSanitizedCommandForTelemetry(['scrape', 'https://example.com'])).toBe('scrape');
67
- expect(getSanitizedCommandForTelemetry(['openapi-check', 'https://swagger.io/spec.json'])).toBe(
68
- 'openapi-check'
69
- );
70
- expect(getSanitizedCommandForTelemetry(['rename', 'a.mdx', 'b.mdx'])).toBe('rename');
71
- expect(getSanitizedCommandForTelemetry(['new', './my-docs'])).toBe('new');
72
- });
73
-
74
- it('includes known analytics subcommands', () => {
75
- expect(getSanitizedCommandForTelemetry(['analytics', 'stats'])).toBe('analytics stats');
76
- expect(getSanitizedCommandForTelemetry(['analytics', 'search'])).toBe('analytics search');
77
- expect(getSanitizedCommandForTelemetry(['analytics', 'feedback'])).toBe('analytics feedback');
78
- expect(getSanitizedCommandForTelemetry(['analytics', 'conversation'])).toBe(
79
- 'analytics conversation'
80
- );
81
- });
82
-
83
- it('includes analytics conversation subcommands', () => {
84
- expect(getSanitizedCommandForTelemetry(['analytics', 'conversation', 'list'])).toBe(
85
- 'analytics conversation list'
86
- );
87
- expect(getSanitizedCommandForTelemetry(['analytics', 'conversation', 'view', 'abc123'])).toBe(
88
- 'analytics conversation view'
89
- );
90
- });
91
-
92
- it('includes analytics conversation buckets subcommands', () => {
93
- expect(getSanitizedCommandForTelemetry(['analytics', 'conversation', 'buckets', 'list'])).toBe(
94
- 'analytics conversation buckets list'
95
- );
96
- expect(
97
- getSanitizedCommandForTelemetry(['analytics', 'conversation', 'buckets', 'view', 'abc123'])
98
- ).toBe('analytics conversation buckets view');
99
- });
100
-
101
- it('includes known config subcommands', () => {
102
- expect(getSanitizedCommandForTelemetry(['config', 'set', 'subdomain', 'my-docs'])).toBe(
103
- 'config set'
104
- );
105
- expect(getSanitizedCommandForTelemetry(['config', 'get', 'subdomain'])).toBe('config get');
106
- expect(getSanitizedCommandForTelemetry(['config', 'clear', 'subdomain'])).toBe('config clear');
107
- });
108
- });
109
-
110
- describe('isTelemetryEnabled', () => {
111
- const savedEnv = process.env;
112
-
113
- beforeEach(() => {
114
- process.env = { ...savedEnv };
115
- delete process.env.MINTLIFY_TELEMETRY_DISABLED;
116
- delete process.env.DO_NOT_TRACK;
117
- delete process.env.CLI_TEST_MODE;
118
- });
119
-
120
- afterEach(() => {
121
- process.env = savedEnv;
122
- });
123
-
124
- it('is enabled by default when config file does not exist', () => {
125
- vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
126
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
127
- });
128
- expect(isTelemetryEnabled()).toBe(true);
129
- });
130
-
131
- it('is disabled when config file has telemetryEnabled: false', () => {
132
- vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ telemetryEnabled: false }));
133
- expect(isTelemetryEnabled()).toBe(false);
134
- });
135
-
136
- it('is enabled when config file has telemetryEnabled: true', () => {
137
- vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ telemetryEnabled: true }));
138
- expect(isTelemetryEnabled()).toBe(true);
139
- });
140
-
141
- it('is disabled via CLI_TEST_MODE=true', () => {
142
- process.env.CLI_TEST_MODE = 'true';
143
- expect(isTelemetryEnabled()).toBe(false);
144
- });
145
-
146
- it('is disabled via MINTLIFY_TELEMETRY_DISABLED=1', () => {
147
- process.env.MINTLIFY_TELEMETRY_DISABLED = '1';
148
- expect(isTelemetryEnabled()).toBe(false);
149
- });
150
-
151
- it('is disabled via DO_NOT_TRACK=1', () => {
152
- process.env.DO_NOT_TRACK = '1';
153
- expect(isTelemetryEnabled()).toBe(false);
154
- });
155
- });
156
-
157
- describe('setTelemetryEnabled', () => {
158
- beforeEach(() => {
159
- vi.mocked(fsExtra.ensureDir).mockResolvedValue(undefined);
160
- });
161
-
162
- it('writes telemetryEnabled into config.json', async () => {
163
- vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
164
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
165
- });
166
- const writeSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => undefined);
167
-
168
- await setTelemetryEnabled(false);
169
-
170
- expect(fsExtra.ensureDir).toHaveBeenCalledWith(expect.stringContaining('mintlify'));
171
- expect(writeSpy).toHaveBeenCalledWith(
172
- expect.stringContaining('config.json'),
173
- JSON.stringify({ telemetryEnabled: false }, null, 2)
174
- );
175
- });
176
-
177
- it('merges with existing config fields', async () => {
178
- vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ someOtherSetting: true }));
179
- const writeSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => undefined);
180
-
181
- await setTelemetryEnabled(false);
182
-
183
- expect(writeSpy).toHaveBeenCalledWith(
184
- expect.stringContaining('config.json'),
185
- JSON.stringify({ someOtherSetting: true, telemetryEnabled: false }, null, 2)
186
- );
187
- });
188
- });
189
-
190
- describe('getDistinctId', () => {
191
- it('returns the persisted UUID', () => {
192
- const id = '550e8400-e29b-41d4-a716-446655440000';
193
- vi.spyOn(fs, 'readFileSync').mockReturnValue(id);
194
- expect(getDistinctId()).toBe(id);
195
- });
196
-
197
- it('generates and persists a new UUID when none exists', () => {
198
- vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
199
- throw new Error('ENOENT');
200
- });
201
- vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
202
- const writeSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => undefined);
203
-
204
- const id = getDistinctId();
205
- expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
206
- expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('anonymous-id'), id, {
207
- flag: 'wx',
208
- });
209
- });
210
-
211
- it('overwrites anonymous-id when it exists but is not a valid UUID', () => {
212
- vi.spyOn(fs, 'readFileSync').mockReturnValue('not-a-uuid');
213
- vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
214
- const writeSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation((...args) => {
215
- const opts = args[2] as { flag?: string } | undefined;
216
- if (opts?.flag === 'wx') {
217
- throw Object.assign(new Error('EEXIST'), { code: 'EEXIST' });
218
- }
219
- });
220
-
221
- const id = getDistinctId();
222
- expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
223
- expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('anonymous-id'), id, {
224
- flag: 'wx',
225
- });
226
- expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('anonymous-id'), id);
227
- });
228
-
229
- it('reuses the same id when the file cannot be persisted (stable machine fallback)', () => {
230
- vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
231
- throw new Error('ENOENT');
232
- });
233
- vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
234
- vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {
235
- throw new Error('EACCES');
236
- });
237
-
238
- const a = getDistinctId();
239
- const b = getDistinctId();
240
- expect(a).toBe(b);
241
- expect(a).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
242
- });
243
- });
244
-
245
- describe('trackCommand', () => {
246
- const savedEnv = process.env;
247
-
248
- beforeEach(() => {
249
- vi.clearAllMocks();
250
- mockCaptureImmediate.mockResolvedValue(undefined);
251
- process.env = { ...savedEnv };
252
- delete process.env.CLI_TEST_MODE;
253
- delete process.env.MINTLIFY_TELEMETRY_DISABLED;
254
- delete process.env.DO_NOT_TRACK;
255
- vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ telemetryEnabled: true }));
256
- });
257
-
258
- afterEach(() => {
259
- process.env = savedEnv;
260
- });
261
-
262
- it('captures event when telemetry is enabled', async () => {
263
- await trackCommand({ command: 'dev', cliVersion: '1.0.0' });
264
-
265
- expect(mockCaptureImmediate).toHaveBeenCalledWith(
266
- expect.objectContaining({
267
- event: 'cli.command.executed',
268
- properties: expect.objectContaining({
269
- command: 'dev',
270
- cli_version: '1.0.0',
271
- os: os.platform(),
272
- }),
273
- })
274
- );
275
- });
276
-
277
- it('does not capture when config has telemetryEnabled: false', async () => {
278
- vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ telemetryEnabled: false }));
279
- await trackCommand({ command: 'dev' });
280
- expect(mockCaptureImmediate).not.toHaveBeenCalled();
281
- });
282
-
283
- it('does not capture when MINTLIFY_TELEMETRY_DISABLED is set', async () => {
284
- process.env.MINTLIFY_TELEMETRY_DISABLED = '1';
285
- await trackCommand({ command: 'dev' });
286
- expect(mockCaptureImmediate).not.toHaveBeenCalled();
287
- });
288
-
289
- it('resolves when captureImmediate hangs after timeout', async () => {
290
- mockCaptureImmediate.mockImplementation(() => new Promise(() => {}));
291
- vi.useFakeTimers();
292
- try {
293
- const done = trackCommand({ command: 'dev' });
294
- await vi.advanceTimersByTimeAsync(TELEMETRY_ASYNC_TIMEOUT_MS);
295
- await expect(done).resolves.toBeUndefined();
296
- } finally {
297
- vi.useRealTimers();
298
- }
299
- });
300
- });
301
-
302
- describe('trackEvent', () => {
303
- const savedEnv = process.env;
304
-
305
- beforeEach(() => {
306
- vi.clearAllMocks();
307
- mockCaptureImmediate.mockResolvedValue(undefined);
308
- process.env = { ...savedEnv };
309
- delete process.env.CLI_TEST_MODE;
310
- delete process.env.MINTLIFY_TELEMETRY_DISABLED;
311
- delete process.env.DO_NOT_TRACK;
312
- vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ telemetryEnabled: true }));
313
- });
314
-
315
- afterEach(() => {
316
- process.env = savedEnv;
317
- });
318
-
319
- it('captures a custom event with standard metadata', async () => {
320
- await trackEvent('cli.dev.started', { subdomain: 'test-docs', port: 3000 });
321
-
322
- expect(mockCaptureImmediate).toHaveBeenCalledWith(
323
- expect.objectContaining({
324
- event: 'cli.dev.started',
325
- properties: expect.objectContaining({
326
- subdomain: 'test-docs',
327
- port: 3000,
328
- cli_version: expect.any(String),
329
- os: os.platform(),
330
- arch: os.arch(),
331
- node_version: process.version,
332
- }),
333
- })
334
- );
335
- });
336
-
337
- it('captures event without extra properties', async () => {
338
- await trackEvent('cli.validate.executed');
339
-
340
- expect(mockCaptureImmediate).toHaveBeenCalledWith(
341
- expect.objectContaining({
342
- event: 'cli.validate.executed',
343
- properties: expect.objectContaining({
344
- os: os.platform(),
345
- }),
346
- })
347
- );
348
- });
349
-
350
- it('does not capture when telemetry is disabled', async () => {
351
- vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ telemetryEnabled: false }));
352
- await trackEvent('cli.dev.started', { subdomain: 'test-docs' });
353
- expect(mockCaptureImmediate).not.toHaveBeenCalled();
354
- });
355
-
356
- it('does not capture when MINTLIFY_TELEMETRY_DISABLED is set', async () => {
357
- process.env.MINTLIFY_TELEMETRY_DISABLED = '1';
358
- await trackEvent('cli.dev.started');
359
- expect(mockCaptureImmediate).not.toHaveBeenCalled();
360
- });
361
-
362
- it('does not throw when captureImmediate rejects', async () => {
363
- mockCaptureImmediate.mockRejectedValue(new Error('network error'));
364
- await expect(trackEvent('cli.dev.started')).resolves.toBeUndefined();
365
- });
366
- });
367
-
368
- describe('trackTelemetryPreferenceChange', () => {
369
- const savedEnv = process.env;
370
-
371
- beforeEach(() => {
372
- vi.clearAllMocks();
373
- mockCaptureImmediate.mockResolvedValue(undefined);
374
- process.env = { ...savedEnv };
375
- delete process.env.CLI_TEST_MODE;
376
- delete process.env.MINTLIFY_TELEMETRY_DISABLED;
377
- });
378
-
379
- afterEach(() => {
380
- process.env = savedEnv;
381
- });
382
-
383
- it('captures cli.telemetry.preference_changed when enabling', async () => {
384
- await trackTelemetryPreferenceChange({ enabled: true });
385
-
386
- expect(mockCaptureImmediate).toHaveBeenCalledWith(
387
- expect.objectContaining({
388
- event: 'cli.telemetry.preference_changed',
389
- properties: expect.objectContaining({ enabled: true, os: os.platform() }),
390
- })
391
- );
392
- });
393
-
394
- it('captures cli.telemetry.preference_changed when disabling', async () => {
395
- await trackTelemetryPreferenceChange({ enabled: false });
396
-
397
- expect(mockCaptureImmediate).toHaveBeenCalledWith(
398
- expect.objectContaining({
399
- event: 'cli.telemetry.preference_changed',
400
- properties: expect.objectContaining({ enabled: false, os: os.platform() }),
401
- })
402
- );
403
- });
404
-
405
- it('fires even when MINTLIFY_TELEMETRY_DISABLED is set (captures opt-out)', async () => {
406
- process.env.MINTLIFY_TELEMETRY_DISABLED = '1';
407
- await trackTelemetryPreferenceChange({ enabled: false });
408
- expect(mockCaptureImmediate).toHaveBeenCalledWith(
409
- expect.objectContaining({ event: 'cli.telemetry.preference_changed' })
410
- );
411
- });
412
-
413
- it('does not capture when CLI_TEST_MODE is set', async () => {
414
- process.env.CLI_TEST_MODE = 'true';
415
- await trackTelemetryPreferenceChange({ enabled: true });
416
- expect(mockCaptureImmediate).not.toHaveBeenCalled();
417
- });
418
- });