@mintlify/cli 4.0.1421 → 4.0.1423
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/__test__/analytics/client.test.ts +180 -0
- package/__test__/analytics/dates.test.ts +12 -0
- package/__test__/analytics/filters.test.ts +116 -0
- package/__test__/analytics/format.test.ts +137 -0
- package/__test__/telemetry.test.ts +27 -0
- package/__test__/welcome.test.ts +15 -0
- package/bin/analytics/client.js +60 -0
- package/bin/analytics/dates.js +6 -0
- package/bin/analytics/filters.js +27 -0
- package/bin/analytics/format.js +13 -0
- package/bin/analytics/index.js +496 -0
- package/bin/analytics/output.js +75 -0
- package/bin/analytics/types.js +1 -0
- package/bin/cli.js +2 -0
- package/bin/middlewares/telemetryMiddleware.js +16 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/bin/welcome.js +4 -1
- package/package.json +8 -8
- package/src/analytics/client.ts +153 -0
- package/src/analytics/dates.ts +6 -0
- package/src/analytics/filters.ts +50 -0
- package/src/analytics/format.ts +13 -0
- package/src/analytics/index.tsx +599 -0
- package/src/analytics/output.ts +99 -0
- package/src/analytics/types.ts +145 -0
- package/src/cli.tsx +2 -0
- package/src/middlewares/telemetryMiddleware.ts +17 -0
- package/src/welcome.ts +4 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAssistantThread,
|
|
3
|
+
getFeedback,
|
|
4
|
+
getFeedbackByPage,
|
|
5
|
+
getBuckets,
|
|
6
|
+
getBucketThreads,
|
|
7
|
+
getConversations,
|
|
8
|
+
getKpi,
|
|
9
|
+
getSearches,
|
|
10
|
+
getViews,
|
|
11
|
+
getVisitors,
|
|
12
|
+
} from '../../src/analytics/client.js';
|
|
13
|
+
|
|
14
|
+
vi.mock('../../src/keyring.js', () => ({
|
|
15
|
+
getAccessToken: vi.fn().mockResolvedValue(null),
|
|
16
|
+
getRefreshToken: vi.fn().mockResolvedValue(null),
|
|
17
|
+
storeCredentials: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const mockFetch = vi.fn();
|
|
21
|
+
global.fetch = mockFetch;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.stubEnv('MINTLIFY_SESSION_TOKEN', 'test-token');
|
|
25
|
+
vi.stubEnv('MINTLIFY_API_URL', 'http://test-server:5000');
|
|
26
|
+
mockFetch.mockReset();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.unstubAllEnvs();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function mockOk(data: unknown) {
|
|
34
|
+
mockFetch.mockResolvedValueOnce({
|
|
35
|
+
ok: true,
|
|
36
|
+
status: 200,
|
|
37
|
+
json: () => Promise.resolve(data),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function mockError(status: number, body: string) {
|
|
42
|
+
mockFetch.mockResolvedValueOnce({
|
|
43
|
+
ok: false,
|
|
44
|
+
status,
|
|
45
|
+
statusText: 'Bad Request',
|
|
46
|
+
text: () => Promise.resolve(body),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function calledUrl(): string {
|
|
51
|
+
const arg = mockFetch.mock.calls[0]![0];
|
|
52
|
+
return typeof arg === 'string' ? arg : String(arg);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function calledUrlObj(): URL {
|
|
56
|
+
return new URL(calledUrl());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('client auth', () => {
|
|
60
|
+
it('throws when no session token is set', async () => {
|
|
61
|
+
vi.stubEnv('MINTLIFY_SESSION_TOKEN', '');
|
|
62
|
+
await expect(getFeedback({ dateFrom: '2024-01-01', dateTo: '2024-01-31' })).rejects.toThrow(
|
|
63
|
+
'Not authenticated'
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('sends session cookie header', async () => {
|
|
68
|
+
mockOk({ feedback: [], nextCursor: null, hasMore: false });
|
|
69
|
+
await getFeedback({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'test');
|
|
70
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
71
|
+
expect.any(String),
|
|
72
|
+
expect.objectContaining({
|
|
73
|
+
headers: expect.objectContaining({
|
|
74
|
+
Authorization: 'Bearer test-token',
|
|
75
|
+
}),
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('client request handling', () => {
|
|
82
|
+
it('throws on API error', async () => {
|
|
83
|
+
mockError(400, 'Invalid params');
|
|
84
|
+
await expect(getFeedback({ dateFrom: '2024-01-01', dateTo: '2024-01-31' })).rejects.toThrow(
|
|
85
|
+
'API error (400): Invalid params'
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('passes subdomain as query param when provided', async () => {
|
|
90
|
+
mockOk({ feedback: [], nextCursor: null, hasMore: false });
|
|
91
|
+
await getFeedback({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'my-docs');
|
|
92
|
+
expect(calledUrlObj().searchParams.get('subdomain')).toBe('my-docs');
|
|
93
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/feedback');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('omits subdomain param when not provided', async () => {
|
|
97
|
+
mockOk({ feedback: [], nextCursor: null, hasMore: false });
|
|
98
|
+
await getFeedback({ dateFrom: '2024-01-01', dateTo: '2024-01-31' });
|
|
99
|
+
expect(calledUrlObj().searchParams.has('subdomain')).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('sets query params and omits undefined values', async () => {
|
|
103
|
+
mockOk({ feedback: [], nextCursor: null, hasMore: false });
|
|
104
|
+
await getFeedback({
|
|
105
|
+
dateFrom: '2024-01-01',
|
|
106
|
+
dateTo: '2024-01-31',
|
|
107
|
+
limit: 10,
|
|
108
|
+
cursor: undefined,
|
|
109
|
+
});
|
|
110
|
+
expect(calledUrlObj().searchParams.get('dateFrom')).toBe('2024-01-01');
|
|
111
|
+
expect(calledUrlObj().searchParams.get('limit')).toBe('10');
|
|
112
|
+
expect(calledUrlObj().searchParams.has('cursor')).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('endpoint paths', () => {
|
|
117
|
+
it('getKpi', async () => {
|
|
118
|
+
mockOk({ humanVisitors: 0 });
|
|
119
|
+
await getKpi({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
120
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/kpi');
|
|
121
|
+
expect(calledUrlObj().searchParams.get('subdomain')).toBe('docs');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('getFeedbackByPage', async () => {
|
|
125
|
+
mockOk({ feedback: [], hasMore: false });
|
|
126
|
+
await getFeedbackByPage({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
127
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/feedback/by-page');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('getConversations', async () => {
|
|
131
|
+
mockOk({ conversations: [], nextCursor: null, hasMore: false });
|
|
132
|
+
await getConversations({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
133
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/assistant');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('getAssistantThread', async () => {
|
|
137
|
+
mockOk({ threadId: 'thread-123', createdAt: '2024-01-01', messages: [] });
|
|
138
|
+
await getAssistantThread('thread/123', 'docs');
|
|
139
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/assistant/threads/thread%2F123');
|
|
140
|
+
expect(calledUrlObj().searchParams.get('subdomain')).toBe('docs');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('getSearches', async () => {
|
|
144
|
+
mockOk({ searches: [], totalSearches: 0, nextCursor: null });
|
|
145
|
+
await getSearches({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
146
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/searches');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('getViews', async () => {
|
|
150
|
+
mockOk({ totals: {}, views: [], hasMore: false });
|
|
151
|
+
await getViews({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
152
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/views');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('getVisitors', async () => {
|
|
156
|
+
mockOk({ totals: {}, visitors: [], hasMore: false });
|
|
157
|
+
await getVisitors({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
158
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/visitors');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('getBuckets', async () => {
|
|
162
|
+
mockOk({ data: [], pagination: { total: 0 } });
|
|
163
|
+
await getBuckets({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
164
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/conversations/buckets');
|
|
165
|
+
expect(calledUrlObj().searchParams.get('subdomain')).toBe('docs');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('getBucketThreads', async () => {
|
|
169
|
+
mockOk({ data: [], pagination: { total: 0, hasMore: false, nextCursor: null } });
|
|
170
|
+
await getBucketThreads(
|
|
171
|
+
'bucket-123',
|
|
172
|
+
{ dateFrom: '2024-01-01', limit: 25, cursor: 'next-thread' },
|
|
173
|
+
'docs'
|
|
174
|
+
);
|
|
175
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/conversations/buckets/bucket-123');
|
|
176
|
+
expect(calledUrlObj().searchParams.get('subdomain')).toBe('docs');
|
|
177
|
+
expect(calledUrlObj().searchParams.get('limit')).toBe('25');
|
|
178
|
+
expect(calledUrlObj().searchParams.get('cursor')).toBe('next-thread');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { formatLocalDate } from '../../src/analytics/dates.js';
|
|
2
|
+
|
|
3
|
+
describe('formatLocalDate', () => {
|
|
4
|
+
it('formats the local calendar date without converting to UTC', () => {
|
|
5
|
+
const date = new Date(2024, 0, 2, 23, 30);
|
|
6
|
+
expect(formatLocalDate(date)).toBe('2024-01-02');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('pads single-digit months and days', () => {
|
|
10
|
+
expect(formatLocalDate(new Date(2024, 8, 7))).toBe('2024-09-07');
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
filterConversationResponse,
|
|
3
|
+
filterFeedbackByPageResponse,
|
|
4
|
+
filterFeedbackResponse,
|
|
5
|
+
filterSearchResponse,
|
|
6
|
+
} from '../../src/analytics/filters.js';
|
|
7
|
+
import type {
|
|
8
|
+
ConversationResponse,
|
|
9
|
+
FeedbackByPageResponse,
|
|
10
|
+
FeedbackResponse,
|
|
11
|
+
SearchResponse,
|
|
12
|
+
} from '../../src/analytics/types.js';
|
|
13
|
+
|
|
14
|
+
describe('analytics response filters', () => {
|
|
15
|
+
it('returns a search response whose JSON rows match query and page filters', () => {
|
|
16
|
+
const data: SearchResponse = {
|
|
17
|
+
searches: [
|
|
18
|
+
{
|
|
19
|
+
searchQuery: 'Deploy a site',
|
|
20
|
+
hits: 5,
|
|
21
|
+
ctr: 40,
|
|
22
|
+
topClickedPage: '/deploy',
|
|
23
|
+
lastSearchedAt: '2024-01-01T00:00:00Z',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
searchQuery: 'Deploy locally',
|
|
27
|
+
hits: 3,
|
|
28
|
+
ctr: 20,
|
|
29
|
+
topClickedPage: '/development',
|
|
30
|
+
lastSearchedAt: '2024-01-02T00:00:00Z',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
searchQuery: 'Authentication',
|
|
34
|
+
hits: 2,
|
|
35
|
+
ctr: 10,
|
|
36
|
+
topClickedPage: '/auth',
|
|
37
|
+
lastSearchedAt: '2024-01-03T00:00:00Z',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
totalSearches: 10,
|
|
41
|
+
nextCursor: null,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
expect(filterSearchResponse(data, { query: 'deploy', page: '/deploy' }).searches).toEqual([
|
|
45
|
+
data.searches[0],
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('filters grouped feedback in the returned response', () => {
|
|
50
|
+
const data: FeedbackByPageResponse = {
|
|
51
|
+
feedback: [
|
|
52
|
+
{ path: '/guide', thumbsUp: 1, thumbsDown: 0, code: 0, total: 1 },
|
|
53
|
+
{ path: '/api', thumbsUp: 0, thumbsDown: 1, code: 0, total: 1 },
|
|
54
|
+
],
|
|
55
|
+
hasMore: false,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
expect(filterFeedbackByPageResponse(data, '/guide').feedback).toEqual([data.feedback[0]]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('filters individual feedback in the returned response', () => {
|
|
62
|
+
const data: FeedbackResponse = {
|
|
63
|
+
feedback: [
|
|
64
|
+
{
|
|
65
|
+
id: '1',
|
|
66
|
+
path: '/guide',
|
|
67
|
+
comment: null,
|
|
68
|
+
createdAt: null,
|
|
69
|
+
source: 'contextual',
|
|
70
|
+
status: 'pending',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: '2',
|
|
74
|
+
path: '/api',
|
|
75
|
+
comment: null,
|
|
76
|
+
createdAt: null,
|
|
77
|
+
source: 'contextual',
|
|
78
|
+
status: 'pending',
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
nextCursor: null,
|
|
82
|
+
hasMore: false,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
expect(filterFeedbackResponse(data, '/guide').feedback).toEqual([data.feedback[0]]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('filters conversations in the returned response', () => {
|
|
89
|
+
const data: ConversationResponse = {
|
|
90
|
+
conversations: [
|
|
91
|
+
{
|
|
92
|
+
id: '1',
|
|
93
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
94
|
+
query: 'Guide question',
|
|
95
|
+
response: 'Answer',
|
|
96
|
+
sources: [{ title: 'Guide', url: 'https://docs.example.com/guide' }],
|
|
97
|
+
queryCategory: null,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: '2',
|
|
101
|
+
timestamp: '2024-01-02T00:00:00Z',
|
|
102
|
+
query: 'API question',
|
|
103
|
+
response: 'Answer',
|
|
104
|
+
sources: [{ title: 'API', url: 'https://docs.example.com/api' }],
|
|
105
|
+
queryCategory: null,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
nextCursor: null,
|
|
109
|
+
hasMore: false,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
expect(filterConversationResponse(data, '/guide').conversations).toEqual([
|
|
113
|
+
data.conversations[0],
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { num, pct, truncate } from '../../src/analytics/format.js';
|
|
2
|
+
import {
|
|
3
|
+
formatBarChart,
|
|
4
|
+
formatPlainTable,
|
|
5
|
+
formatPrettyTable,
|
|
6
|
+
resolveFormat,
|
|
7
|
+
} from '../../src/analytics/output.js';
|
|
8
|
+
import * as helpers from '../../src/helpers.js';
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('num', () => {
|
|
15
|
+
it('formats numbers with locale separators', () => {
|
|
16
|
+
expect(num(0)).toBe('0');
|
|
17
|
+
expect(num(1234)).toBe('1,234');
|
|
18
|
+
expect(num(1000000)).toBe('1,000,000');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('pct', () => {
|
|
23
|
+
it('returns percentage string', () => {
|
|
24
|
+
expect(pct(1, 4)).toBe('25.0%');
|
|
25
|
+
expect(pct(1, 3)).toBe('33.3%');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns dash when total is zero', () => {
|
|
29
|
+
expect(pct(0, 0)).toBe('\u2014');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('truncate', () => {
|
|
34
|
+
it('returns short strings unchanged', () => {
|
|
35
|
+
expect(truncate('hello', 10)).toBe('hello');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('truncates long strings with ellipsis', () => {
|
|
39
|
+
expect(truncate('hello world', 6)).toBe('hello\u2026');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns string unchanged when exactly at max', () => {
|
|
43
|
+
expect(truncate('hello', 5)).toBe('hello');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('formatPrettyTable', () => {
|
|
48
|
+
it('returns dim message for empty rows', () => {
|
|
49
|
+
const result = formatPrettyTable(['A', 'B'], []);
|
|
50
|
+
expect(result).toContain('No data found');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('formats headers and rows with aligned columns', () => {
|
|
54
|
+
const result = formatPrettyTable(
|
|
55
|
+
['Name', 'Count'],
|
|
56
|
+
[
|
|
57
|
+
['foo', '10'],
|
|
58
|
+
['barbaz', '5'],
|
|
59
|
+
]
|
|
60
|
+
);
|
|
61
|
+
const lines = result.split('\n');
|
|
62
|
+
expect(lines).toHaveLength(4);
|
|
63
|
+
expect(lines[2]).toContain('foo');
|
|
64
|
+
expect(lines[3]).toContain('barbaz');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('formatPlainTable', () => {
|
|
69
|
+
it('returns empty string for empty rows', () => {
|
|
70
|
+
expect(formatPlainTable(['A', 'B'], [])).toBe('');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('formats with tab separation and uppercase headers', () => {
|
|
74
|
+
const result = formatPlainTable(
|
|
75
|
+
['Name', 'Count'],
|
|
76
|
+
[
|
|
77
|
+
['foo', '10'],
|
|
78
|
+
['bar', '5'],
|
|
79
|
+
]
|
|
80
|
+
);
|
|
81
|
+
const lines = result.split('\n');
|
|
82
|
+
expect(lines[0]).toContain('NAME');
|
|
83
|
+
expect(lines[0]).toContain('\t');
|
|
84
|
+
expect(lines[1]).toContain('foo');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('resolveFormat', () => {
|
|
89
|
+
it('returns json when AI mode is active', () => {
|
|
90
|
+
vi.spyOn(helpers, 'isAI').mockReturnValue(true);
|
|
91
|
+
expect(resolveFormat({})).toBe('json');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns specified format', () => {
|
|
95
|
+
vi.spyOn(helpers, 'isAI').mockReturnValue(true);
|
|
96
|
+
expect(resolveFormat({ format: 'plain' })).toBe('plain');
|
|
97
|
+
expect(resolveFormat({ format: 'json' })).toBe('json');
|
|
98
|
+
expect(resolveFormat({ format: 'graph' })).toBe('graph');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('defaults to plain', () => {
|
|
102
|
+
vi.spyOn(helpers, 'isAI').mockReturnValue(false);
|
|
103
|
+
expect(resolveFormat({})).toBe('plain');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('formatBarChart', () => {
|
|
108
|
+
it('returns dim message for empty items', () => {
|
|
109
|
+
expect(formatBarChart([])).toContain('No data found');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('renders bars with labels and values', () => {
|
|
113
|
+
const result = formatBarChart([
|
|
114
|
+
{ label: 'Foo', value: 10 },
|
|
115
|
+
{ label: 'Bar', value: 5 },
|
|
116
|
+
]);
|
|
117
|
+
const lines = result.split('\n');
|
|
118
|
+
expect(lines).toHaveLength(2);
|
|
119
|
+
expect(lines[0]).toContain('Foo');
|
|
120
|
+
expect(lines[0]).toContain('10');
|
|
121
|
+
expect(lines[0]).toContain('\u2588');
|
|
122
|
+
expect(lines[1]).toContain('Bar');
|
|
123
|
+
expect(lines[1]).toContain('5');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('scales bars relative to max value', () => {
|
|
127
|
+
const result = formatBarChart([
|
|
128
|
+
{ label: 'Big', value: 100 },
|
|
129
|
+
{ label: 'Small', value: 10 },
|
|
130
|
+
]);
|
|
131
|
+
const bigBar = result.split('\n')[0]!;
|
|
132
|
+
const smallBar = result.split('\n')[1]!;
|
|
133
|
+
const bigBlocks = (bigBar.match(/\u2588/g) || []).length;
|
|
134
|
+
const smallBlocks = (smallBar.match(/\u2588/g) || []).length;
|
|
135
|
+
expect(bigBlocks).toBeGreaterThan(smallBlocks);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -71,6 +71,33 @@ describe('getSanitizedCommandForTelemetry', () => {
|
|
|
71
71
|
expect(getSanitizedCommandForTelemetry(['new', './my-docs'])).toBe('new');
|
|
72
72
|
});
|
|
73
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
|
+
|
|
74
101
|
it('includes known config subcommands', () => {
|
|
75
102
|
expect(getSanitizedCommandForTelemetry(['config', 'set', 'subdomain', 'my-docs'])).toBe(
|
|
76
103
|
'config set'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { renderWelcome } from '../src/welcome.js';
|
|
2
|
+
|
|
3
|
+
describe('welcome help menu', () => {
|
|
4
|
+
it('lists the analytics command under Insights', () => {
|
|
5
|
+
const output = renderWelcome({
|
|
6
|
+
packageName: 'mint',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
signedIn: true,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(output).toContain('Insights');
|
|
12
|
+
expect(output).toContain('analytics');
|
|
13
|
+
expect(output).toContain('View analytics for your site');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { authenticatedFetch } from '../authenticatedFetch.js';
|
|
11
|
+
import { API_URL } from '../constants.js';
|
|
12
|
+
function request(path_1) {
|
|
13
|
+
return __awaiter(this, arguments, void 0, function* (path, params = {}) {
|
|
14
|
+
const url = new URL(`${API_URL}/api/cli/analytics${path}`);
|
|
15
|
+
for (const [key, value] of Object.entries(params)) {
|
|
16
|
+
if (value !== undefined)
|
|
17
|
+
url.searchParams.set(key, String(value));
|
|
18
|
+
}
|
|
19
|
+
const res = yield authenticatedFetch(url.toString(), {
|
|
20
|
+
headers: { Accept: 'application/json' },
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
const body = yield res.text().catch(() => '');
|
|
24
|
+
throw new Error(`API error (${res.status}): ${body || res.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
return res.json();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function getKpi(opts, subdomain) {
|
|
30
|
+
return request('/kpi', Object.assign(Object.assign({}, opts), { subdomain }));
|
|
31
|
+
}
|
|
32
|
+
export function getFeedback(opts, subdomain) {
|
|
33
|
+
return request('/feedback', Object.assign(Object.assign({}, opts), { subdomain }));
|
|
34
|
+
}
|
|
35
|
+
export function getFeedbackByPage(opts, subdomain) {
|
|
36
|
+
return request('/feedback/by-page', Object.assign(Object.assign({}, opts), { subdomain }));
|
|
37
|
+
}
|
|
38
|
+
export function getConversations(opts, subdomain) {
|
|
39
|
+
return request('/assistant', Object.assign(Object.assign({}, opts), { subdomain }));
|
|
40
|
+
}
|
|
41
|
+
export function getAssistantThread(threadId, subdomain) {
|
|
42
|
+
return request(`/assistant/threads/${encodeURIComponent(threadId)}`, {
|
|
43
|
+
subdomain,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function getSearches(opts, subdomain) {
|
|
47
|
+
return request('/searches', Object.assign(Object.assign({}, opts), { subdomain }));
|
|
48
|
+
}
|
|
49
|
+
export function getViews(opts, subdomain) {
|
|
50
|
+
return request('/views', Object.assign(Object.assign({}, opts), { subdomain }));
|
|
51
|
+
}
|
|
52
|
+
export function getVisitors(opts, subdomain) {
|
|
53
|
+
return request('/visitors', Object.assign(Object.assign({}, opts), { subdomain }));
|
|
54
|
+
}
|
|
55
|
+
export function getBuckets(opts, subdomain) {
|
|
56
|
+
return request('/conversations/buckets', Object.assign(Object.assign({}, opts), { subdomain }));
|
|
57
|
+
}
|
|
58
|
+
export function getBucketThreads(bucketId, opts, subdomain) {
|
|
59
|
+
return request(`/conversations/buckets/${encodeURIComponent(bucketId)}`, Object.assign(Object.assign({}, opts), { subdomain }));
|
|
60
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function filterSearchResponse(data, filters) {
|
|
2
|
+
let searches = data.searches;
|
|
3
|
+
if (filters.query) {
|
|
4
|
+
const query = filters.query.toLowerCase();
|
|
5
|
+
searches = searches.filter((row) => row.searchQuery.toLowerCase().includes(query));
|
|
6
|
+
}
|
|
7
|
+
const page = filters.page;
|
|
8
|
+
if (page) {
|
|
9
|
+
searches = searches.filter((row) => { var _a; return (_a = row.topClickedPage) === null || _a === void 0 ? void 0 : _a.includes(page); });
|
|
10
|
+
}
|
|
11
|
+
return Object.assign(Object.assign({}, data), { searches });
|
|
12
|
+
}
|
|
13
|
+
export function filterFeedbackByPageResponse(data, page) {
|
|
14
|
+
if (!page)
|
|
15
|
+
return data;
|
|
16
|
+
return Object.assign(Object.assign({}, data), { feedback: data.feedback.filter((row) => row.path.includes(page)) });
|
|
17
|
+
}
|
|
18
|
+
export function filterFeedbackResponse(data, page) {
|
|
19
|
+
if (!page)
|
|
20
|
+
return data;
|
|
21
|
+
return Object.assign(Object.assign({}, data), { feedback: data.feedback.filter((row) => row.path.includes(page)) });
|
|
22
|
+
}
|
|
23
|
+
export function filterConversationResponse(data, page) {
|
|
24
|
+
if (!page)
|
|
25
|
+
return data;
|
|
26
|
+
return Object.assign(Object.assign({}, data), { conversations: data.conversations.filter((conversation) => conversation.sources.some((source) => source.url.includes(page))) });
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function num(n) {
|
|
2
|
+
return n.toLocaleString('en-US');
|
|
3
|
+
}
|
|
4
|
+
export function pct(n, total) {
|
|
5
|
+
if (total === 0)
|
|
6
|
+
return '\u2014';
|
|
7
|
+
return ((n / total) * 100).toFixed(1) + '%';
|
|
8
|
+
}
|
|
9
|
+
export function truncate(s, max) {
|
|
10
|
+
if (s.length <= max)
|
|
11
|
+
return s;
|
|
12
|
+
return s.slice(0, max - 1) + '\u2026';
|
|
13
|
+
}
|