@contentful/experience-design-system-cli 2.23.2-dev-build-b213913.0 → 2.23.2-dev-build-8b2cfc9.0
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/README.md +23 -0
- package/dist/package.json +2 -1
- package/dist/src/analytics/apply.d.ts +6 -0
- package/dist/src/analytics/apply.js +30 -0
- package/dist/src/analytics/client.d.ts +3 -0
- package/dist/src/analytics/client.js +39 -0
- package/dist/src/analytics/index.d.ts +6 -0
- package/dist/src/analytics/index.js +5 -0
- package/dist/src/analytics/normalize.d.ts +3 -0
- package/dist/src/analytics/normalize.js +17 -0
- package/dist/src/analytics/os.d.ts +2 -0
- package/dist/src/analytics/os.js +13 -0
- package/dist/src/analytics/tracker.d.ts +19 -0
- package/dist/src/analytics/tracker.js +157 -0
- package/dist/src/analytics/types.d.ts +29 -0
- package/dist/src/analytics/types.js +1 -0
- package/dist/src/analyze/command.js +4 -0
- package/dist/src/apply/api-client.d.ts +3 -0
- package/dist/src/apply/api-client.js +14 -1
- package/dist/src/apply/command.js +22 -5
- package/dist/src/import/orchestrator.js +6 -1
- package/dist/src/index.js +5 -1
- package/dist/src/program.js +8 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -450,6 +450,29 @@ Wizard run history is separate: `~/.config/experiences/runs.json`.
|
|
|
450
450
|
|
|
451
451
|
---
|
|
452
452
|
|
|
453
|
+
## Usage data
|
|
454
|
+
|
|
455
|
+
The CLI collects **anonymous usage data** to help us understand which commands are used and where the import workflow succeeds or fails. This data does **not** include your source code, file paths, credentials, prompts, or any content you author.
|
|
456
|
+
|
|
457
|
+
What may be included:
|
|
458
|
+
|
|
459
|
+
- Command name and duration
|
|
460
|
+
- CLI, Node.js, and operating-system version
|
|
461
|
+
- Anonymous session identifiers that link steps within a single import run
|
|
462
|
+
- Structural counts (for example, how many components were extracted or accepted)
|
|
463
|
+
- Space and environment IDs you pass on the command line
|
|
464
|
+
- Contentful request IDs from API responses (to correlate failures with server logs)
|
|
465
|
+
|
|
466
|
+
You can turn this off at any time:
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
DISABLE_ANALYTICS=1 experiences import --project ./my-app
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Setting `DISABLE_ANALYTICS` to any value disables collection for that invocation.
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
453
476
|
## Development
|
|
454
477
|
|
|
455
478
|
```bash
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experience-design-system-cli",
|
|
3
|
-
"version": "2.23.2-dev-build-
|
|
3
|
+
"version": "2.23.2-dev-build-8b2cfc9.0",
|
|
4
4
|
"description": "Contentful Experiences design system import CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"@contentful/experience-design-system-extraction": "workspace:*",
|
|
42
42
|
"@contentful/experience-design-system-generation": "workspace:*",
|
|
43
43
|
"@contentful/experience-design-system-types": "workspace:*",
|
|
44
|
+
"@segment/analytics-node": "^3.0.0",
|
|
44
45
|
"commander": "^13.1.0",
|
|
45
46
|
"ink": "^4.4.1",
|
|
46
47
|
"react": "^18.3.1",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ApplyOperationResponse } from '@contentful/experience-design-system-types';
|
|
2
|
+
import type { ImportApiClient } from '../apply/api-client.js';
|
|
3
|
+
/** Record apply/preview API outcomes on the active command. */
|
|
4
|
+
export declare function recordApplyOutcome(client: ImportApiClient, spaceId: string, environmentId: string, operation: ApplyOperationResponse): void;
|
|
5
|
+
/** Record Contentful target context without an apply operation (e.g. preview-only). */
|
|
6
|
+
export declare function recordContentfulContext(client: ImportApiClient, spaceId: string, environmentId: string): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { enrichCommandResult, setCommandContext } from './tracker.js';
|
|
2
|
+
function countWriteResult(items, entityType) {
|
|
3
|
+
const subset = items.filter((item) => item.entityType === entityType);
|
|
4
|
+
return {
|
|
5
|
+
created_count: subset.filter((item) => item.action === 'create' && item.status === 'succeeded').length,
|
|
6
|
+
updated_count: subset.filter((item) => item.action === 'update' && item.status === 'succeeded').length,
|
|
7
|
+
failed_count: subset.filter((item) => item.status === 'failed').length,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** Record apply/preview API outcomes on the active command. */
|
|
11
|
+
export function recordApplyOutcome(client, spaceId, environmentId, operation) {
|
|
12
|
+
setCommandContext({
|
|
13
|
+
space_key: spaceId,
|
|
14
|
+
environment_key: environmentId,
|
|
15
|
+
x_contentful_request_id: client.getLastRequestId(),
|
|
16
|
+
});
|
|
17
|
+
enrichCommandResult({
|
|
18
|
+
dsi_operation_id: operation.sys.id,
|
|
19
|
+
component_type_result: countWriteResult(operation.items ?? [], 'ComponentType'),
|
|
20
|
+
design_token_result: countWriteResult(operation.items ?? [], 'DesignToken'),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** Record Contentful target context without an apply operation (e.g. preview-only). */
|
|
24
|
+
export function recordContentfulContext(client, spaceId, environmentId) {
|
|
25
|
+
setCommandContext({
|
|
26
|
+
space_key: spaceId,
|
|
27
|
+
environment_key: environmentId,
|
|
28
|
+
x_contentful_request_id: client.getLastRequestId(),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { Analytics } from '@segment/analytics-node';
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const pkg = require('../../package.json');
|
|
5
|
+
// Anonymous usage telemetry for the CLI. Disabled when DISABLE_ANALYTICS is set
|
|
6
|
+
// or when no write key is configured. Never blocks command execution.
|
|
7
|
+
export function cliVersion() {
|
|
8
|
+
return pkg.version;
|
|
9
|
+
}
|
|
10
|
+
export function analyticsEnabled() {
|
|
11
|
+
return !process.env.DISABLE_ANALYTICS && Boolean(resolveWriteKey());
|
|
12
|
+
}
|
|
13
|
+
function resolveWriteKey() {
|
|
14
|
+
const key = (process.env.SEGMENT_WRITE_KEY ?? '').trim();
|
|
15
|
+
return key.length > 0 ? key : undefined;
|
|
16
|
+
}
|
|
17
|
+
export async function trackEvent(event, properties, anonymousId) {
|
|
18
|
+
if (!analyticsEnabled())
|
|
19
|
+
return;
|
|
20
|
+
const writeKey = resolveWriteKey();
|
|
21
|
+
if (!writeKey)
|
|
22
|
+
return;
|
|
23
|
+
try {
|
|
24
|
+
const client = new Analytics({ writeKey });
|
|
25
|
+
client.on('error', () => {
|
|
26
|
+
/* never block the CLI on telemetry errors */
|
|
27
|
+
});
|
|
28
|
+
client.track({
|
|
29
|
+
event,
|
|
30
|
+
properties,
|
|
31
|
+
anonymousId,
|
|
32
|
+
timestamp: new Date(),
|
|
33
|
+
});
|
|
34
|
+
await client.closeAndFlush();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Telemetry must never affect CLI exit codes or output.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { analyticsEnabled, cliVersion, trackEvent } from './client.js';
|
|
2
|
+
export { normalizeCommand } from './normalize.js';
|
|
3
|
+
export { getOsName } from './os.js';
|
|
4
|
+
export { bindAnalyticsSession, bindAnalyticsSessionId, completeActiveCommand, emitSessionStarted, enrichCommandResult, failActiveCommand, getBoundSessionId, noteCommandStart, registerAnalyticsExitHook, resetAnalyticsStateForTests, setCommandContext, } from './tracker.js';
|
|
5
|
+
export { recordApplyOutcome, recordContentfulContext } from './apply.js';
|
|
6
|
+
export type { CommandCompletion, CommandContext, CommandFailure, DsiCliCommand, EntryCommand, OsName, WriteResult, } from './types.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { analyticsEnabled, cliVersion, trackEvent } from './client.js';
|
|
2
|
+
export { normalizeCommand } from './normalize.js';
|
|
3
|
+
export { getOsName } from './os.js';
|
|
4
|
+
export { bindAnalyticsSession, bindAnalyticsSessionId, completeActiveCommand, emitSessionStarted, enrichCommandResult, failActiveCommand, getBoundSessionId, noteCommandStart, registerAnalyticsExitHook, resetAnalyticsStateForTests, setCommandContext, } from './tracker.js';
|
|
5
|
+
export { recordApplyOutcome, recordContentfulContext } from './apply.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const COMMAND_MAP = {
|
|
2
|
+
'analyze extract': 'analyze_extract',
|
|
3
|
+
'analyze select': 'analyze_select',
|
|
4
|
+
'generate components': 'generate_components',
|
|
5
|
+
'generate tokens': 'generate_tokens',
|
|
6
|
+
'generate edit': 'generate_edit',
|
|
7
|
+
'apply preview': 'apply_preview',
|
|
8
|
+
'apply select': 'apply_select',
|
|
9
|
+
'apply push': 'apply_push',
|
|
10
|
+
'print components': 'print_components',
|
|
11
|
+
'print tokens': 'print_tokens',
|
|
12
|
+
import: 'import',
|
|
13
|
+
};
|
|
14
|
+
/** Map a Commander command chain (e.g. "apply push") to a tracked command id, if any. */
|
|
15
|
+
export function normalizeCommand(commandChain) {
|
|
16
|
+
return COMMAND_MAP[commandChain];
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const OS_NAMES = {
|
|
2
|
+
android: 'Android',
|
|
3
|
+
aix: 'Linux',
|
|
4
|
+
darwin: 'macOS',
|
|
5
|
+
freebsd: 'Linux',
|
|
6
|
+
linux: 'Linux',
|
|
7
|
+
openbsd: 'Linux',
|
|
8
|
+
sunos: 'Linux',
|
|
9
|
+
win32: 'Windows',
|
|
10
|
+
};
|
|
11
|
+
export function getOsName() {
|
|
12
|
+
return OS_NAMES[process.platform] ?? 'other';
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CommandCompletion, CommandContext, CommandFailure, EntryCommand } from './types.js';
|
|
2
|
+
/** Record the start of a tracked command. Invoked is deferred until a session is bound. */
|
|
3
|
+
export declare function noteCommandStart(commandChain: string): void;
|
|
4
|
+
/** Bind a session id, minting one when the command has no pipeline session. */
|
|
5
|
+
export declare function bindAnalyticsSessionId(sessionId: string | undefined, context?: CommandContext): Promise<string>;
|
|
6
|
+
/** Attach Contentful target context for the active command. */
|
|
7
|
+
export declare function setCommandContext(context: CommandContext): void;
|
|
8
|
+
/** Merge optional completion fields before the terminal event fires. */
|
|
9
|
+
export declare function enrichCommandResult(fields: CommandCompletion): void;
|
|
10
|
+
export declare function getBoundSessionId(): string | undefined;
|
|
11
|
+
/** Bind the pipeline session and emit invoked once both command and session are known. */
|
|
12
|
+
export declare function bindAnalyticsSession(id: string, context?: CommandContext): Promise<void>;
|
|
13
|
+
/** Emit session_started once per new pipeline head. */
|
|
14
|
+
export declare function emitSessionStarted(entryCommand: EntryCommand): Promise<void>;
|
|
15
|
+
export declare function completeActiveCommand(): Promise<void>;
|
|
16
|
+
export declare function failActiveCommand(fields?: CommandFailure): Promise<void>;
|
|
17
|
+
/** Best-effort terminal event when the process exits without a clean postAction hook. */
|
|
18
|
+
export declare function registerAnalyticsExitHook(): void;
|
|
19
|
+
export declare function resetAnalyticsStateForTests(): void;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { cliVersion, trackEvent } from './client.js';
|
|
2
|
+
import { generateSessionId } from '../session/session-id.js';
|
|
3
|
+
import { normalizeCommand } from './normalize.js';
|
|
4
|
+
import { getOsName } from './os.js';
|
|
5
|
+
let pending = null;
|
|
6
|
+
let sessionId;
|
|
7
|
+
let sessionStartedEmitted = false;
|
|
8
|
+
function isPipelineStep() {
|
|
9
|
+
return process.env.EDS_IMPORT_PIPELINE === '1';
|
|
10
|
+
}
|
|
11
|
+
function buildBaseProps(command) {
|
|
12
|
+
return {
|
|
13
|
+
dsi_session_id: sessionId,
|
|
14
|
+
command,
|
|
15
|
+
...(isPipelineStep() ? { is_pipeline_step: true } : {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function mergeContext(target, source) {
|
|
19
|
+
if (source.space_key !== undefined)
|
|
20
|
+
target.space_key = source.space_key;
|
|
21
|
+
if (source.environment_key !== undefined)
|
|
22
|
+
target.environment_key = source.environment_key;
|
|
23
|
+
if (source.x_contentful_request_id !== undefined) {
|
|
24
|
+
target.x_contentful_request_id = source.x_contentful_request_id;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Record the start of a tracked command. Invoked is deferred until a session is bound. */
|
|
28
|
+
export function noteCommandStart(commandChain) {
|
|
29
|
+
const command = normalizeCommand(commandChain);
|
|
30
|
+
if (!command)
|
|
31
|
+
return;
|
|
32
|
+
pending = {
|
|
33
|
+
command,
|
|
34
|
+
startedAt: Date.now(),
|
|
35
|
+
invoked: false,
|
|
36
|
+
terminalEmitted: false,
|
|
37
|
+
context: {},
|
|
38
|
+
completion: {},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Bind a session id, minting one when the command has no pipeline session. */
|
|
42
|
+
export async function bindAnalyticsSessionId(sessionId, context) {
|
|
43
|
+
const id = sessionId ?? generateSessionId();
|
|
44
|
+
await bindAnalyticsSession(id, context);
|
|
45
|
+
return id;
|
|
46
|
+
}
|
|
47
|
+
/** Attach Contentful target context for the active command. */
|
|
48
|
+
export function setCommandContext(context) {
|
|
49
|
+
if (!pending)
|
|
50
|
+
return;
|
|
51
|
+
mergeContext(pending.context, context);
|
|
52
|
+
mergeContext(pending.completion, context);
|
|
53
|
+
}
|
|
54
|
+
/** Merge optional completion fields before the terminal event fires. */
|
|
55
|
+
export function enrichCommandResult(fields) {
|
|
56
|
+
if (!pending)
|
|
57
|
+
return;
|
|
58
|
+
Object.assign(pending.completion, fields);
|
|
59
|
+
mergeContext(pending.context, fields);
|
|
60
|
+
}
|
|
61
|
+
export function getBoundSessionId() {
|
|
62
|
+
return sessionId;
|
|
63
|
+
}
|
|
64
|
+
/** Bind the pipeline session and emit invoked once both command and session are known. */
|
|
65
|
+
export async function bindAnalyticsSession(id, context) {
|
|
66
|
+
sessionId = id;
|
|
67
|
+
if (context)
|
|
68
|
+
setCommandContext(context);
|
|
69
|
+
await emitInvokedIfReady();
|
|
70
|
+
}
|
|
71
|
+
/** Emit session_started once per new pipeline head. */
|
|
72
|
+
export async function emitSessionStarted(entryCommand) {
|
|
73
|
+
if (!sessionId || sessionStartedEmitted)
|
|
74
|
+
return;
|
|
75
|
+
sessionStartedEmitted = true;
|
|
76
|
+
await trackEvent('dsi_cli_session_started', {
|
|
77
|
+
dsi_session_id: sessionId,
|
|
78
|
+
entry_command: entryCommand,
|
|
79
|
+
cli_version: cliVersion(),
|
|
80
|
+
node_version: process.version,
|
|
81
|
+
os_name: getOsName(),
|
|
82
|
+
}, sessionId);
|
|
83
|
+
await emitInvokedIfReady();
|
|
84
|
+
}
|
|
85
|
+
async function emitInvokedIfReady() {
|
|
86
|
+
if (!pending || pending.invoked || !sessionId)
|
|
87
|
+
return;
|
|
88
|
+
pending.invoked = true;
|
|
89
|
+
await trackEvent('dsi_cli_command_invoked', {
|
|
90
|
+
...buildBaseProps(pending.command),
|
|
91
|
+
...pending.context,
|
|
92
|
+
}, sessionId);
|
|
93
|
+
}
|
|
94
|
+
export async function completeActiveCommand() {
|
|
95
|
+
if (!pending || pending.terminalEmitted || !sessionId || !pending.invoked)
|
|
96
|
+
return;
|
|
97
|
+
pending.terminalEmitted = true;
|
|
98
|
+
const durationMs = Date.now() - pending.startedAt;
|
|
99
|
+
await trackEvent('dsi_cli_command_completed', {
|
|
100
|
+
...buildBaseProps(pending.command),
|
|
101
|
+
...pending.context,
|
|
102
|
+
...pending.completion,
|
|
103
|
+
outcome: 'ok',
|
|
104
|
+
duration_ms: durationMs,
|
|
105
|
+
}, sessionId);
|
|
106
|
+
}
|
|
107
|
+
export async function failActiveCommand(fields = {}) {
|
|
108
|
+
if (!pending || pending.terminalEmitted || !sessionId || !pending.invoked)
|
|
109
|
+
return;
|
|
110
|
+
pending.terminalEmitted = true;
|
|
111
|
+
const durationMs = Date.now() - pending.startedAt;
|
|
112
|
+
const outcome = fields.exit_code === 130 ? 'interrupted' : 'error';
|
|
113
|
+
await trackEvent('dsi_cli_command_failed', {
|
|
114
|
+
...buildBaseProps(pending.command),
|
|
115
|
+
...pending.context,
|
|
116
|
+
...fields,
|
|
117
|
+
outcome,
|
|
118
|
+
duration_ms: durationMs,
|
|
119
|
+
}, sessionId);
|
|
120
|
+
}
|
|
121
|
+
let exitHookRegistered = false;
|
|
122
|
+
/** Best-effort terminal event when the process exits without a clean postAction hook. */
|
|
123
|
+
export function registerAnalyticsExitHook() {
|
|
124
|
+
if (exitHookRegistered)
|
|
125
|
+
return;
|
|
126
|
+
exitHookRegistered = true;
|
|
127
|
+
process.on('exit', (code) => {
|
|
128
|
+
if (!pending || pending.terminalEmitted || !sessionId || !pending.invoked)
|
|
129
|
+
return;
|
|
130
|
+
const durationMs = Date.now() - pending.startedAt;
|
|
131
|
+
const outcome = code === 0 ? 'ok' : code === 130 ? 'interrupted' : 'error';
|
|
132
|
+
const event = code === 0 ? 'dsi_cli_command_completed' : 'dsi_cli_command_failed';
|
|
133
|
+
const props = code === 0
|
|
134
|
+
? {
|
|
135
|
+
...buildBaseProps(pending.command),
|
|
136
|
+
...pending.context,
|
|
137
|
+
...pending.completion,
|
|
138
|
+
outcome: 'ok',
|
|
139
|
+
duration_ms: durationMs,
|
|
140
|
+
}
|
|
141
|
+
: {
|
|
142
|
+
...buildBaseProps(pending.command),
|
|
143
|
+
...pending.context,
|
|
144
|
+
outcome: outcome === 'interrupted' ? 'interrupted' : 'error',
|
|
145
|
+
duration_ms: durationMs,
|
|
146
|
+
exit_code: code,
|
|
147
|
+
};
|
|
148
|
+
// Synchronous best-effort — async flush cannot run on 'exit'.
|
|
149
|
+
void trackEvent(event, props, sessionId);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
export function resetAnalyticsStateForTests() {
|
|
153
|
+
pending = null;
|
|
154
|
+
sessionId = undefined;
|
|
155
|
+
sessionStartedEmitted = false;
|
|
156
|
+
exitHookRegistered = false;
|
|
157
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Tracked CLI commands — closed set aligned with the published event schema. */
|
|
2
|
+
export type DsiCliCommand = 'analyze_extract' | 'analyze_select' | 'generate_components' | 'generate_tokens' | 'generate_edit' | 'apply_preview' | 'apply_select' | 'apply_push' | 'print_components' | 'print_tokens' | 'import';
|
|
3
|
+
export type EntryCommand = 'analyze_extract' | 'import';
|
|
4
|
+
export type OsName = 'macOS' | 'Linux' | 'Windows' | 'Android' | 'other';
|
|
5
|
+
export type WriteResult = {
|
|
6
|
+
created_count: number;
|
|
7
|
+
updated_count: number;
|
|
8
|
+
failed_count: number;
|
|
9
|
+
};
|
|
10
|
+
export type CommandContext = {
|
|
11
|
+
space_key?: string;
|
|
12
|
+
environment_key?: string;
|
|
13
|
+
x_contentful_request_id?: string;
|
|
14
|
+
};
|
|
15
|
+
export type CommandCompletion = CommandContext & {
|
|
16
|
+
dsi_operation_id?: string;
|
|
17
|
+
extracted_component_count?: number;
|
|
18
|
+
accepted_component_count?: number;
|
|
19
|
+
component_type_result?: WriteResult;
|
|
20
|
+
design_token_result?: WriteResult;
|
|
21
|
+
};
|
|
22
|
+
export type CommandFailure = CommandContext & {
|
|
23
|
+
dsi_operation_id?: string;
|
|
24
|
+
error_name?: string;
|
|
25
|
+
error_code?: string;
|
|
26
|
+
http_status_code?: number;
|
|
27
|
+
x_contentful_request_id?: string;
|
|
28
|
+
exit_code?: number;
|
|
29
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -24,6 +24,7 @@ import { DEFAULT_AGENT_NAME, isAgentName, runAgent, } from '@contentful/experien
|
|
|
24
24
|
import { readExperiencesCredentials } from '../credentials-store.js';
|
|
25
25
|
import { buildAnalyzeViewRows, partitionGlobalWarnings } from './build-analyze-view-rows.js';
|
|
26
26
|
import { getInteractiveTerminalSupport } from '../lib/terminal-capabilities.js';
|
|
27
|
+
import { bindAnalyticsSession, emitSessionStarted, enrichCommandResult } from '../analytics/index.js';
|
|
27
28
|
import { getDebugLogger } from '../lib/debug-logger.js';
|
|
28
29
|
const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.svelte', '.ts', '.tsx', '.vue']);
|
|
29
30
|
/**
|
|
@@ -264,6 +265,8 @@ export function registerAnalyzeCommand(program) {
|
|
|
264
265
|
inputPath: projectRoot,
|
|
265
266
|
outDir,
|
|
266
267
|
});
|
|
268
|
+
await bindAnalyticsSession(sessionId);
|
|
269
|
+
await emitSessionStarted('analyze_extract');
|
|
267
270
|
const stepId = createStep(db, sessionId, 'analyze extract', {
|
|
268
271
|
project: projectRoot,
|
|
269
272
|
});
|
|
@@ -537,6 +540,7 @@ export function registerAnalyzeCommand(program) {
|
|
|
537
540
|
storeSlotCycles(db, sessionId, withBreaks);
|
|
538
541
|
storeScannedFiles(db, sessionId, sourceFiles.map((f) => relative(projectRoot, f)));
|
|
539
542
|
updateStep(db, stepId, 'complete', { sessionId });
|
|
543
|
+
enrichCommandResult({ extracted_component_count: validatedComponents.length });
|
|
540
544
|
db.close();
|
|
541
545
|
const allWarnings = [...extraction.warnings, ...filterWarnings];
|
|
542
546
|
const { rows: componentRows, totalErrors } = buildAnalyzeViewRows(filteredComponents, validatedComponents, allWarnings);
|
|
@@ -45,6 +45,9 @@ export declare class ImportApiClient {
|
|
|
45
45
|
private spaceId;
|
|
46
46
|
private environmentId;
|
|
47
47
|
private retry;
|
|
48
|
+
private lastRequestId?;
|
|
49
|
+
getLastRequestId(): string | undefined;
|
|
50
|
+
private noteRequestId;
|
|
48
51
|
constructor(opts: ApiClientOptions);
|
|
49
52
|
private base;
|
|
50
53
|
private headers;
|
|
@@ -180,6 +180,15 @@ export class ImportApiClient {
|
|
|
180
180
|
spaceId;
|
|
181
181
|
environmentId;
|
|
182
182
|
retry;
|
|
183
|
+
lastRequestId;
|
|
184
|
+
getLastRequestId() {
|
|
185
|
+
return this.lastRequestId;
|
|
186
|
+
}
|
|
187
|
+
noteRequestId(response) {
|
|
188
|
+
const requestId = response.headers.get('x-contentful-request-id');
|
|
189
|
+
if (requestId)
|
|
190
|
+
this.lastRequestId = requestId;
|
|
191
|
+
}
|
|
183
192
|
constructor(opts) {
|
|
184
193
|
this.host = toApiHost(opts.host);
|
|
185
194
|
this.token = opts.cmaToken;
|
|
@@ -237,8 +246,10 @@ export class ImportApiClient {
|
|
|
237
246
|
await this.retry.sleep(delayMs);
|
|
238
247
|
continue;
|
|
239
248
|
}
|
|
240
|
-
if (!isTransientStatus(result.response.status))
|
|
249
|
+
if (!isTransientStatus(result.response.status)) {
|
|
250
|
+
this.noteRequestId(result.response);
|
|
241
251
|
return result;
|
|
252
|
+
}
|
|
242
253
|
if (attempt === this.retry.maxAttempts) {
|
|
243
254
|
const body = stringifyError(result.error);
|
|
244
255
|
const guidance = `The ${phase} request failed after ${attempt} attempts because the service remained unavailable. ` +
|
|
@@ -273,6 +284,7 @@ export class ImportApiClient {
|
|
|
273
284
|
// false positives that don't apply to the design-systems API authorization path.
|
|
274
285
|
const url = `${this.host}/users/me`;
|
|
275
286
|
const res = await request(url, { token: this.token });
|
|
287
|
+
this.noteRequestId(res);
|
|
276
288
|
if (res.status === 401) {
|
|
277
289
|
throw new ApiError('CMA token is invalid or revoked', res.status, await res.text());
|
|
278
290
|
}
|
|
@@ -298,6 +310,7 @@ export class ImportApiClient {
|
|
|
298
310
|
catch {
|
|
299
311
|
return; // network error — don't block; the real call will surface it
|
|
300
312
|
}
|
|
313
|
+
this.noteRequestId(res);
|
|
301
314
|
if (res.ok)
|
|
302
315
|
return;
|
|
303
316
|
if (res.status >= 500)
|
|
@@ -16,7 +16,9 @@ import { addArtifactInputOptions, addCompositionOptions, addContentfulTargetOpti
|
|
|
16
16
|
import { stripAllowedComponents } from '../import/strip-allowed-components.js';
|
|
17
17
|
import { readExperiencesCredentials } from '../credentials-store.js';
|
|
18
18
|
import { getInteractiveTerminalSupport, requireInteractiveTerminal } from '../lib/terminal-capabilities.js';
|
|
19
|
+
import { bindAnalyticsSessionId, completeActiveCommand, failActiveCommand, recordApplyOutcome, recordContentfulContext, } from '../analytics/index.js';
|
|
19
20
|
function die(message) {
|
|
21
|
+
void failActiveCommand({ exit_code: 1 });
|
|
20
22
|
process.stderr.write(`${message}\n`);
|
|
21
23
|
process.exit(1);
|
|
22
24
|
}
|
|
@@ -431,6 +433,12 @@ export function registerApplyCommand(program) {
|
|
|
431
433
|
throw e;
|
|
432
434
|
}
|
|
433
435
|
const { components, tokens, client } = inputs;
|
|
436
|
+
const spaceId = opts.spaceId;
|
|
437
|
+
const environmentId = opts.environmentId;
|
|
438
|
+
await bindAnalyticsSessionId(opts.session, {
|
|
439
|
+
space_key: spaceId,
|
|
440
|
+
environment_key: environmentId,
|
|
441
|
+
});
|
|
434
442
|
try {
|
|
435
443
|
await client.validateToken();
|
|
436
444
|
}
|
|
@@ -450,8 +458,7 @@ export function registerApplyCommand(program) {
|
|
|
450
458
|
die(`Error: ${formatApiError(e)}`);
|
|
451
459
|
throw e;
|
|
452
460
|
}
|
|
453
|
-
|
|
454
|
-
const environmentId = opts.environmentId;
|
|
461
|
+
recordContentfulContext(client, spaceId, environmentId);
|
|
455
462
|
if (getInteractiveTerminalSupport().supported) {
|
|
456
463
|
const { waitUntilExit } = render(createElement(ServerPreviewApp, {
|
|
457
464
|
preview,
|
|
@@ -490,6 +497,12 @@ export function registerApplyCommand(program) {
|
|
|
490
497
|
throw e;
|
|
491
498
|
}
|
|
492
499
|
const { components, tokens, client } = inputs;
|
|
500
|
+
const spaceId = opts.spaceId;
|
|
501
|
+
const environmentId = opts.environmentId;
|
|
502
|
+
await bindAnalyticsSessionId(opts.session, {
|
|
503
|
+
space_key: spaceId,
|
|
504
|
+
environment_key: environmentId,
|
|
505
|
+
});
|
|
493
506
|
assertNoSlotCycles(components);
|
|
494
507
|
try {
|
|
495
508
|
await client.validateToken();
|
|
@@ -509,8 +522,6 @@ export function registerApplyCommand(program) {
|
|
|
509
522
|
die(`Error: ${formatApiError(e, opts.verbose)}`);
|
|
510
523
|
throw e;
|
|
511
524
|
}
|
|
512
|
-
const spaceId = opts.spaceId;
|
|
513
|
-
const environmentId = opts.environmentId;
|
|
514
525
|
if (opts.dryRun) {
|
|
515
526
|
if (isTTY) {
|
|
516
527
|
const { waitUntilExit } = render(createElement(ServerPreviewApp, {
|
|
@@ -564,8 +575,14 @@ export function registerApplyCommand(program) {
|
|
|
564
575
|
throw e;
|
|
565
576
|
}
|
|
566
577
|
const summary = buildApplyOutput(operation, spaceId, environmentId, opts.host);
|
|
578
|
+
recordApplyOutcome(client, spaceId, environmentId, operation);
|
|
567
579
|
process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
|
|
568
|
-
|
|
580
|
+
const exitCode = operation.sys.status === 'succeeded' ? 0 : 1;
|
|
581
|
+
if (exitCode === 0)
|
|
582
|
+
await completeActiveCommand();
|
|
583
|
+
else
|
|
584
|
+
await failActiveCommand({ exit_code: exitCode });
|
|
585
|
+
process.exit(exitCode);
|
|
569
586
|
return;
|
|
570
587
|
}
|
|
571
588
|
await new Promise((resolvePromise) => {
|
|
@@ -7,6 +7,7 @@ import { detectSlotCycles, formatSlotCycleReport } from '../apply/command.js';
|
|
|
7
7
|
import { PREVIEW_ERROR_PREFIX, VALIDATION_FAILED_CODE, parsePreviewValidationErrors } from '../apply/api-client.js';
|
|
8
8
|
import { buildPostPushUrl } from '../lib/contentful-urls.js';
|
|
9
9
|
import { getDebugLogger, debugEnvForSubprocess } from '../lib/debug-logger.js';
|
|
10
|
+
import { bindAnalyticsSession, emitSessionStarted } from '../analytics/index.js';
|
|
10
11
|
function findCliPath() {
|
|
11
12
|
return join(fileURLToPath(import.meta.url), '..', '..', '..', '..', 'bin', 'cli.js');
|
|
12
13
|
}
|
|
@@ -16,7 +17,7 @@ async function runStep(args, cliPath, env = {}, streamStderr = false) {
|
|
|
16
17
|
debug.event('import', 'subprocess.spawn', { cliPath, args });
|
|
17
18
|
return new Promise((res) => {
|
|
18
19
|
const child = execFile('node', [cliPath, ...args], {
|
|
19
|
-
env: debugEnvForSubprocess({ ...process.env, ...env }),
|
|
20
|
+
env: debugEnvForSubprocess({ ...process.env, ...env, EDS_IMPORT_PIPELINE: '1' }),
|
|
20
21
|
});
|
|
21
22
|
let stdout = '';
|
|
22
23
|
let stderr = '';
|
|
@@ -108,6 +109,10 @@ export async function runPipeline(opts, progressWriter, cliPathOverride) {
|
|
|
108
109
|
inputPath: projectRoot,
|
|
109
110
|
outDir,
|
|
110
111
|
});
|
|
112
|
+
await bindAnalyticsSession(sessionId, {
|
|
113
|
+
...(opts.spaceId ? { space_key: opts.spaceId, environment_key: opts.environmentId } : {}),
|
|
114
|
+
});
|
|
115
|
+
await emitSessionStarted('import');
|
|
111
116
|
progressWriter(`Experience Design System CLI — Pipeline Import`);
|
|
112
117
|
progressWriter(`Project: ${projectRoot}`);
|
|
113
118
|
progressWriter(`Output: ${outDir}`);
|
package/dist/src/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { createProgram } from './program.js';
|
|
2
|
+
import { failActiveCommand } from './analytics/index.js';
|
|
2
3
|
createProgram()
|
|
3
4
|
.parseAsync()
|
|
4
|
-
.catch((err) => {
|
|
5
|
+
.catch(async (err) => {
|
|
6
|
+
await failActiveCommand({
|
|
7
|
+
error_name: err instanceof Error ? err.name : 'Error',
|
|
8
|
+
});
|
|
5
9
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
6
10
|
process.exit(1);
|
|
7
11
|
});
|
package/dist/src/program.js
CHANGED
|
@@ -12,6 +12,7 @@ import { registerImportCommand } from './import/command.js';
|
|
|
12
12
|
import { registerSetupCommand } from './setup/command.js';
|
|
13
13
|
import { registerRunsCommand } from './runs/ls-command.js';
|
|
14
14
|
import { beginCommand } from './lib/debug-preamble.js';
|
|
15
|
+
import { completeActiveCommand, noteCommandStart, registerAnalyticsExitHook } from './analytics/index.js';
|
|
15
16
|
const require = createRequire(import.meta.url);
|
|
16
17
|
const pkg = require('../package.json');
|
|
17
18
|
/**
|
|
@@ -73,6 +74,7 @@ export function createProgram() {
|
|
|
73
74
|
// and a bright-green "debug logs at <path>" banner is printed to stderr.
|
|
74
75
|
program.option('--debug', 'Write a JSONL trace of every decision to ~/.contentful/experience-design-system-cli/debug/');
|
|
75
76
|
program.option('--no-debug', 'Force debug logging off (overrides EDSI_DEBUG and persisted setup preference)');
|
|
77
|
+
registerAnalyticsExitHook();
|
|
76
78
|
program.hook('preAction', async (_thisCommand, actionCommand) => {
|
|
77
79
|
// Merge opts from actionCommand and all ancestors — root-level --debug
|
|
78
80
|
// set alongside a subcommand ends up on the root command's opts, not the
|
|
@@ -89,7 +91,12 @@ export function createProgram() {
|
|
|
89
91
|
const chain = [];
|
|
90
92
|
for (let c = actionCommand; c && c.parent; c = c.parent)
|
|
91
93
|
chain.unshift(c.name());
|
|
92
|
-
|
|
94
|
+
const commandChain = chain.join(' ') || actionCommand.name();
|
|
95
|
+
noteCommandStart(commandChain);
|
|
96
|
+
await beginCommand(commandChain, { ...(debug !== undefined ? { debug } : {}) });
|
|
97
|
+
});
|
|
98
|
+
program.hook('postAction', async () => {
|
|
99
|
+
await completeActiveCommand();
|
|
93
100
|
});
|
|
94
101
|
return program;
|
|
95
102
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experience-design-system-cli",
|
|
3
|
-
"version": "2.23.2-dev-build-
|
|
3
|
+
"version": "2.23.2-dev-build-8b2cfc9.0",
|
|
4
4
|
"description": "Contentful Experiences design system import CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,15 +28,16 @@
|
|
|
28
28
|
"prompts/"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@segment/analytics-node": "^3.0.0",
|
|
31
32
|
"commander": "^13.1.0",
|
|
32
33
|
"ink": "^4.4.1",
|
|
33
34
|
"react": "^18.3.1",
|
|
34
35
|
"react-devtools-core": "^4.19.1",
|
|
35
36
|
"react-dom": "^18.3.1",
|
|
36
|
-
"@contentful/experience-design-system-client": "2.23.2-dev-build-
|
|
37
|
-
"@contentful/experience-design-system-extraction": "2.23.2-dev-build-
|
|
38
|
-
"@contentful/experience-design-system-generation": "2.23.2-dev-build-
|
|
39
|
-
"@contentful/experience-design-system-types": "2.23.2-dev-build-
|
|
37
|
+
"@contentful/experience-design-system-client": "2.23.2-dev-build-8b2cfc9.0",
|
|
38
|
+
"@contentful/experience-design-system-extraction": "2.23.2-dev-build-8b2cfc9.0",
|
|
39
|
+
"@contentful/experience-design-system-generation": "2.23.2-dev-build-8b2cfc9.0",
|
|
40
|
+
"@contentful/experience-design-system-types": "2.23.2-dev-build-8b2cfc9.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@tsconfig/node24": "^24.0.4",
|