@contentful/experience-design-system-cli 2.24.0 → 2.24.1-dev-build-58e271c.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 +7 -0
- package/dist/src/analytics/client.js +68 -0
- package/dist/src/analytics/constants.d.ts +4 -0
- package/dist/src/analytics/constants.js +4 -0
- package/dist/src/analytics/env.d.ts +4 -0
- package/dist/src/analytics/env.js +14 -0
- package/dist/src/analytics/exit.d.ts +5 -0
- package/dist/src/analytics/exit.js +24 -0
- package/dist/src/analytics/index.d.ts +10 -0
- package/dist/src/analytics/index.js +9 -0
- package/dist/src/analytics/normalize.d.ts +3 -0
- package/dist/src/analytics/normalize.js +18 -0
- package/dist/src/analytics/os.d.ts +2 -0
- package/dist/src/analytics/os.js +13 -0
- package/dist/src/analytics/session.d.ts +3 -0
- package/dist/src/analytics/session.js +15 -0
- package/dist/src/analytics/tracker.d.ts +17 -0
- package/dist/src/analytics/tracker.js +126 -0
- package/dist/src/analytics/types.d.ts +28 -0
- package/dist/src/analytics/types.js +1 -0
- package/dist/src/analyze/command.js +8 -2
- package/dist/src/analyze/select/command.js +13 -9
- package/dist/src/analyze/select-agent/command.js +14 -11
- package/dist/src/apply/api-client.d.ts +3 -0
- package/dist/src/apply/api-client.js +14 -1
- package/dist/src/apply/command.d.ts +1 -1
- package/dist/src/apply/command.js +71 -50
- package/dist/src/generate/command.js +15 -9
- package/dist/src/generate/edit/command.js +1 -0
- package/dist/src/import/orchestrator.js +18 -12
- package/dist/src/index.js +6 -1
- package/dist/src/print/command.js +16 -13
- package/dist/src/program.js +8 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -451,6 +451,29 @@ Wizard run history is separate: `~/.config/experiences/runs.json`.
|
|
|
451
451
|
|
|
452
452
|
---
|
|
453
453
|
|
|
454
|
+
## Usage data
|
|
455
|
+
|
|
456
|
+
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.
|
|
457
|
+
|
|
458
|
+
What may be included:
|
|
459
|
+
|
|
460
|
+
- Command name and duration
|
|
461
|
+
- CLI, Node.js, and operating-system version
|
|
462
|
+
- Anonymous session identifiers that link steps within a single import run
|
|
463
|
+
- Structural counts (for example, how many components were extracted or accepted)
|
|
464
|
+
- Space and environment IDs you pass on the command line
|
|
465
|
+
- Contentful request IDs from API responses (to correlate failures with server logs)
|
|
466
|
+
|
|
467
|
+
You can turn this off at any time:
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
DISABLE_ANALYTICS=1 experiences import --project ./my-app
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Setting `DISABLE_ANALYTICS` to any value disables collection for that invocation.
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
454
477
|
## Development
|
|
455
478
|
|
|
456
479
|
```bash
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experience-design-system-cli",
|
|
3
|
-
"version": "2.24.0",
|
|
3
|
+
"version": "2.24.1-dev-build-58e271c.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,7 @@
|
|
|
1
|
+
export declare function cliVersion(): string;
|
|
2
|
+
export declare function analyticsEnabled(): boolean;
|
|
3
|
+
export declare function trackEvent(event: string, properties: Record<string, unknown>, anonymousId: string, options?: {
|
|
4
|
+
flush?: boolean;
|
|
5
|
+
}): Promise<void>;
|
|
6
|
+
export declare function flushAnalytics(): Promise<void>;
|
|
7
|
+
export declare function resetAnalyticsClientForTests(): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
let analyticsClient = null;
|
|
8
|
+
export function cliVersion() {
|
|
9
|
+
return pkg.version;
|
|
10
|
+
}
|
|
11
|
+
export function analyticsEnabled() {
|
|
12
|
+
return !process.env.DISABLE_ANALYTICS && Boolean(resolveWriteKey());
|
|
13
|
+
}
|
|
14
|
+
function resolveWriteKey() {
|
|
15
|
+
const key = (process.env.SEGMENT_WRITE_KEY ?? '').trim();
|
|
16
|
+
return key.length > 0 ? key : undefined;
|
|
17
|
+
}
|
|
18
|
+
function getClient() {
|
|
19
|
+
if (!analyticsEnabled())
|
|
20
|
+
return null;
|
|
21
|
+
const writeKey = resolveWriteKey();
|
|
22
|
+
if (!writeKey)
|
|
23
|
+
return null;
|
|
24
|
+
if (!analyticsClient) {
|
|
25
|
+
analyticsClient = new Analytics({ writeKey });
|
|
26
|
+
analyticsClient.on('error', () => {
|
|
27
|
+
/* never block the CLI on telemetry errors */
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return analyticsClient;
|
|
31
|
+
}
|
|
32
|
+
export async function trackEvent(event, properties, anonymousId, options) {
|
|
33
|
+
const client = getClient();
|
|
34
|
+
if (!client)
|
|
35
|
+
return;
|
|
36
|
+
try {
|
|
37
|
+
client.track({
|
|
38
|
+
event,
|
|
39
|
+
properties,
|
|
40
|
+
anonymousId,
|
|
41
|
+
timestamp: new Date(),
|
|
42
|
+
});
|
|
43
|
+
if (options?.flush)
|
|
44
|
+
await flushAnalytics();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Telemetry must never affect CLI exit codes or output.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// CLI processes are short-lived — we flush (and recreate the client) after each
|
|
51
|
+
// event so terminal telemetry survives process.exit. Do not "optimize" this into
|
|
52
|
+
// a deferred batch flush on process exit; Node will not await async exit hooks.
|
|
53
|
+
export async function flushAnalytics() {
|
|
54
|
+
if (!analyticsClient)
|
|
55
|
+
return;
|
|
56
|
+
try {
|
|
57
|
+
await analyticsClient.closeAndFlush();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Swallow — telemetry must never affect CLI behavior.
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
analyticsClient = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function resetAnalyticsClientForTests() {
|
|
67
|
+
analyticsClient = null;
|
|
68
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Parent pipeline session id propagated to orchestrator subprocesses. */
|
|
2
|
+
export declare const ANALYTICS_SESSION_ENV = "EDS_ANALYTICS_SESSION_ID";
|
|
3
|
+
/** Set when a command is spawned by the import orchestrator. */
|
|
4
|
+
export declare const IMPORT_PIPELINE_ENV = "EDS_IMPORT_PIPELINE";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Merge pipeline analytics env into a subprocess environment. */
|
|
2
|
+
export declare function analyticsEnvForSubprocess(env: NodeJS.ProcessEnv, analyticsSessionId: string): NodeJS.ProcessEnv;
|
|
3
|
+
/** Debug + pipeline analytics env for orchestrator subprocesses. */
|
|
4
|
+
export declare function pipelineSubprocessEnv(env: NodeJS.ProcessEnv, analyticsSessionId: string): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { debugEnvForSubprocess } from '../lib/debug-logger.js';
|
|
2
|
+
import { ANALYTICS_SESSION_ENV, IMPORT_PIPELINE_ENV } from './constants.js';
|
|
3
|
+
/** Merge pipeline analytics env into a subprocess environment. */
|
|
4
|
+
export function analyticsEnvForSubprocess(env, analyticsSessionId) {
|
|
5
|
+
return {
|
|
6
|
+
...env,
|
|
7
|
+
[IMPORT_PIPELINE_ENV]: '1',
|
|
8
|
+
[ANALYTICS_SESSION_ENV]: analyticsSessionId,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Debug + pipeline analytics env for orchestrator subprocesses. */
|
|
12
|
+
export function pipelineSubprocessEnv(env, analyticsSessionId) {
|
|
13
|
+
return analyticsEnvForSubprocess(debugEnvForSubprocess(env), analyticsSessionId);
|
|
14
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ApiError } from '../apply/api-client.js';
|
|
2
|
+
import type { CommandFailure } from './types.js';
|
|
3
|
+
export declare function exitWithAnalytics(code: number, fields?: CommandFailure): Promise<never>;
|
|
4
|
+
export declare function failureFromApiError(error: ApiError): CommandFailure;
|
|
5
|
+
export declare function failureFromUnknown(error: unknown): CommandFailure;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { flushAnalytics } from './client.js';
|
|
2
|
+
import { completeActiveCommand, failActiveCommand } from './tracker.js';
|
|
3
|
+
export async function exitWithAnalytics(code, fields = {}) {
|
|
4
|
+
if (code === 0)
|
|
5
|
+
await completeActiveCommand();
|
|
6
|
+
else
|
|
7
|
+
await failActiveCommand({ exit_code: code, ...fields });
|
|
8
|
+
await flushAnalytics();
|
|
9
|
+
process.exit(code);
|
|
10
|
+
throw new Error('unreachable');
|
|
11
|
+
}
|
|
12
|
+
export function failureFromApiError(error) {
|
|
13
|
+
return {
|
|
14
|
+
error_name: error.name,
|
|
15
|
+
http_status_code: error.status,
|
|
16
|
+
exit_code: 1,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function failureFromUnknown(error) {
|
|
20
|
+
if (error instanceof Error) {
|
|
21
|
+
return { error_name: error.name, exit_code: 1 };
|
|
22
|
+
}
|
|
23
|
+
return { error_name: 'Error', exit_code: 1 };
|
|
24
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { ANALYTICS_SESSION_ENV, IMPORT_PIPELINE_ENV } from './constants.js';
|
|
2
|
+
export { analyticsEnvForSubprocess, pipelineSubprocessEnv } from './env.js';
|
|
3
|
+
export { exitWithAnalytics, failureFromApiError, failureFromUnknown } from './exit.js';
|
|
4
|
+
export { analyticsEnabled, cliVersion, flushAnalytics, resetAnalyticsClientForTests, trackEvent } from './client.js';
|
|
5
|
+
export { isPipelineAnalyticsChild, resolveAnalyticsSessionId } from './session.js';
|
|
6
|
+
export { normalizeCommand } from './normalize.js';
|
|
7
|
+
export { getOsName } from './os.js';
|
|
8
|
+
export { bindAnalyticsSession, bindAnalyticsSessionId, completeActiveCommand, emitSessionStarted, enrichCommandResult, failActiveCommand, getBoundSessionId, noteCommandStart, resetAnalyticsStateForTests, setCommandContext, } from './tracker.js';
|
|
9
|
+
export { recordApplyOutcome, recordContentfulContext } from './apply.js';
|
|
10
|
+
export type { CommandCompletion, CommandContext, CommandFailure, DsiCliCommand, EntryCommand, OsName, WriteResult, } from './types.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { ANALYTICS_SESSION_ENV, IMPORT_PIPELINE_ENV } from './constants.js';
|
|
2
|
+
export { analyticsEnvForSubprocess, pipelineSubprocessEnv } from './env.js';
|
|
3
|
+
export { exitWithAnalytics, failureFromApiError, failureFromUnknown } from './exit.js';
|
|
4
|
+
export { analyticsEnabled, cliVersion, flushAnalytics, resetAnalyticsClientForTests, trackEvent } from './client.js';
|
|
5
|
+
export { isPipelineAnalyticsChild, resolveAnalyticsSessionId } from './session.js';
|
|
6
|
+
export { normalizeCommand } from './normalize.js';
|
|
7
|
+
export { getOsName } from './os.js';
|
|
8
|
+
export { bindAnalyticsSession, bindAnalyticsSessionId, completeActiveCommand, emitSessionStarted, enrichCommandResult, failActiveCommand, getBoundSessionId, noteCommandStart, resetAnalyticsStateForTests, setCommandContext, } from './tracker.js';
|
|
9
|
+
export { recordApplyOutcome, recordContentfulContext } from './apply.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const COMMAND_MAP = {
|
|
2
|
+
'analyze extract': 'analyze_extract',
|
|
3
|
+
'analyze select': 'analyze_select',
|
|
4
|
+
'analyze select-agent': 'analyze_select',
|
|
5
|
+
'generate components': 'generate_components',
|
|
6
|
+
'generate tokens': 'generate_tokens',
|
|
7
|
+
'generate edit': 'generate_edit',
|
|
8
|
+
'apply preview': 'apply_preview',
|
|
9
|
+
'apply select': 'apply_select',
|
|
10
|
+
'apply push': 'apply_push',
|
|
11
|
+
'print components': 'print_components',
|
|
12
|
+
'print tokens': 'print_tokens',
|
|
13
|
+
import: 'import',
|
|
14
|
+
};
|
|
15
|
+
/** Map a Commander command chain (e.g. "apply push") to a tracked command id, if any. */
|
|
16
|
+
export function normalizeCommand(commandChain) {
|
|
17
|
+
return COMMAND_MAP[commandChain];
|
|
18
|
+
}
|
|
@@ -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,15 @@
|
|
|
1
|
+
import { generateSessionId } from '../session/session-id.js';
|
|
2
|
+
import { ANALYTICS_SESSION_ENV } from './constants.js';
|
|
3
|
+
/** Prefer the pipeline parent session, then an explicit id, then a new slug. */
|
|
4
|
+
export function resolveAnalyticsSessionId(explicit) {
|
|
5
|
+
const inherited = process.env[ANALYTICS_SESSION_ENV]?.trim();
|
|
6
|
+
if (inherited)
|
|
7
|
+
return inherited;
|
|
8
|
+
const passed = explicit?.trim();
|
|
9
|
+
if (passed)
|
|
10
|
+
return passed;
|
|
11
|
+
return generateSessionId();
|
|
12
|
+
}
|
|
13
|
+
export function isPipelineAnalyticsChild() {
|
|
14
|
+
return Boolean(process.env[ANALYTICS_SESSION_ENV]?.trim());
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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, preferring a pipeline parent id when present. */
|
|
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
|
+
export declare function resetAnalyticsStateForTests(): void;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { cliVersion, trackEvent } from './client.js';
|
|
2
|
+
import { IMPORT_PIPELINE_ENV } from './constants.js';
|
|
3
|
+
import { normalizeCommand } from './normalize.js';
|
|
4
|
+
import { getOsName } from './os.js';
|
|
5
|
+
import { resolveAnalyticsSessionId } from './session.js';
|
|
6
|
+
let pending = null;
|
|
7
|
+
let sessionId;
|
|
8
|
+
let sessionStartedEmitted = false;
|
|
9
|
+
function isPipelineStep() {
|
|
10
|
+
return process.env[IMPORT_PIPELINE_ENV] === '1';
|
|
11
|
+
}
|
|
12
|
+
function buildBaseProps(command) {
|
|
13
|
+
return {
|
|
14
|
+
dsi_session_id: sessionId,
|
|
15
|
+
command,
|
|
16
|
+
...(isPipelineStep() ? { is_pipeline_step: true } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function mergeContext(target, source) {
|
|
20
|
+
if (source.space_key !== undefined)
|
|
21
|
+
target.space_key = source.space_key;
|
|
22
|
+
if (source.environment_key !== undefined)
|
|
23
|
+
target.environment_key = source.environment_key;
|
|
24
|
+
if (source.x_contentful_request_id !== undefined) {
|
|
25
|
+
target.x_contentful_request_id = source.x_contentful_request_id;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Record the start of a tracked command. Invoked is deferred until a session is bound. */
|
|
29
|
+
export function noteCommandStart(commandChain) {
|
|
30
|
+
const command = normalizeCommand(commandChain);
|
|
31
|
+
if (!command)
|
|
32
|
+
return;
|
|
33
|
+
pending = {
|
|
34
|
+
command,
|
|
35
|
+
startedAt: Date.now(),
|
|
36
|
+
invoked: false,
|
|
37
|
+
terminalEmitted: false,
|
|
38
|
+
context: {},
|
|
39
|
+
completion: {},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Bind a session id, preferring a pipeline parent id when present. */
|
|
43
|
+
export async function bindAnalyticsSessionId(sessionId, context) {
|
|
44
|
+
const id = resolveAnalyticsSessionId(sessionId);
|
|
45
|
+
await bindAnalyticsSession(id, context);
|
|
46
|
+
return id;
|
|
47
|
+
}
|
|
48
|
+
/** Attach Contentful target context for the active command. */
|
|
49
|
+
export function setCommandContext(context) {
|
|
50
|
+
if (!pending)
|
|
51
|
+
return;
|
|
52
|
+
mergeContext(pending.context, context);
|
|
53
|
+
mergeContext(pending.completion, context);
|
|
54
|
+
}
|
|
55
|
+
/** Merge optional completion fields before the terminal event fires. */
|
|
56
|
+
export function enrichCommandResult(fields) {
|
|
57
|
+
if (!pending)
|
|
58
|
+
return;
|
|
59
|
+
Object.assign(pending.completion, fields);
|
|
60
|
+
mergeContext(pending.context, fields);
|
|
61
|
+
}
|
|
62
|
+
export function getBoundSessionId() {
|
|
63
|
+
return sessionId;
|
|
64
|
+
}
|
|
65
|
+
/** Bind the pipeline session and emit invoked once both command and session are known. */
|
|
66
|
+
export async function bindAnalyticsSession(id, context) {
|
|
67
|
+
sessionId = id;
|
|
68
|
+
if (context)
|
|
69
|
+
setCommandContext(context);
|
|
70
|
+
await emitInvokedIfReady();
|
|
71
|
+
}
|
|
72
|
+
/** Emit session_started once per new pipeline head. */
|
|
73
|
+
export async function emitSessionStarted(entryCommand) {
|
|
74
|
+
if (!sessionId || sessionStartedEmitted)
|
|
75
|
+
return;
|
|
76
|
+
sessionStartedEmitted = true;
|
|
77
|
+
await trackEvent('dsi_cli_session_started', {
|
|
78
|
+
dsi_session_id: sessionId,
|
|
79
|
+
entry_command: entryCommand,
|
|
80
|
+
cli_version: cliVersion(),
|
|
81
|
+
node_version: process.version,
|
|
82
|
+
os_name: getOsName(),
|
|
83
|
+
}, sessionId, { flush: true });
|
|
84
|
+
await emitInvokedIfReady();
|
|
85
|
+
}
|
|
86
|
+
async function emitInvokedIfReady() {
|
|
87
|
+
if (!pending || pending.invoked || !sessionId)
|
|
88
|
+
return;
|
|
89
|
+
pending.invoked = true;
|
|
90
|
+
await trackEvent('dsi_cli_command_invoked', {
|
|
91
|
+
...buildBaseProps(pending.command),
|
|
92
|
+
...pending.context,
|
|
93
|
+
}, sessionId, { flush: true });
|
|
94
|
+
}
|
|
95
|
+
export async function completeActiveCommand() {
|
|
96
|
+
if (!pending || pending.terminalEmitted || !sessionId || !pending.invoked)
|
|
97
|
+
return;
|
|
98
|
+
pending.terminalEmitted = true;
|
|
99
|
+
const durationMs = Date.now() - pending.startedAt;
|
|
100
|
+
await trackEvent('dsi_cli_command_completed', {
|
|
101
|
+
...buildBaseProps(pending.command),
|
|
102
|
+
...pending.context,
|
|
103
|
+
...pending.completion,
|
|
104
|
+
outcome: 'ok',
|
|
105
|
+
duration_ms: durationMs,
|
|
106
|
+
}, sessionId, { flush: true });
|
|
107
|
+
}
|
|
108
|
+
export async function failActiveCommand(fields = {}) {
|
|
109
|
+
if (!pending || pending.terminalEmitted || !sessionId || !pending.invoked)
|
|
110
|
+
return;
|
|
111
|
+
pending.terminalEmitted = true;
|
|
112
|
+
const durationMs = Date.now() - pending.startedAt;
|
|
113
|
+
const outcome = fields.exit_code === 130 ? 'interrupted' : 'error';
|
|
114
|
+
await trackEvent('dsi_cli_command_failed', {
|
|
115
|
+
...buildBaseProps(pending.command),
|
|
116
|
+
...pending.context,
|
|
117
|
+
...fields,
|
|
118
|
+
outcome,
|
|
119
|
+
duration_ms: durationMs,
|
|
120
|
+
}, sessionId, { flush: true });
|
|
121
|
+
}
|
|
122
|
+
export function resetAnalyticsStateForTests() {
|
|
123
|
+
pending = null;
|
|
124
|
+
sessionId = undefined;
|
|
125
|
+
sessionStartedEmitted = false;
|
|
126
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
exit_code?: number;
|
|
28
|
+
};
|
|
@@ -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 { bindAnalyticsSessionId, emitSessionStarted, enrichCommandResult, exitWithAnalytics, isPipelineAnalyticsChild, } 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,10 @@ export function registerAnalyzeCommand(program) {
|
|
|
264
265
|
inputPath: projectRoot,
|
|
265
266
|
outDir,
|
|
266
267
|
});
|
|
268
|
+
await bindAnalyticsSessionId(sessionId);
|
|
269
|
+
if (!isPipelineAnalyticsChild()) {
|
|
270
|
+
await emitSessionStarted('analyze_extract');
|
|
271
|
+
}
|
|
267
272
|
const stepId = createStep(db, sessionId, 'analyze extract', {
|
|
268
273
|
project: projectRoot,
|
|
269
274
|
});
|
|
@@ -537,6 +542,7 @@ export function registerAnalyzeCommand(program) {
|
|
|
537
542
|
storeSlotCycles(db, sessionId, withBreaks);
|
|
538
543
|
storeScannedFiles(db, sessionId, sourceFiles.map((f) => relative(projectRoot, f)));
|
|
539
544
|
updateStep(db, stepId, 'complete', { sessionId });
|
|
545
|
+
enrichCommandResult({ extracted_component_count: validatedComponents.length });
|
|
540
546
|
db.close();
|
|
541
547
|
const allWarnings = [...extraction.warnings, ...filterWarnings];
|
|
542
548
|
const { rows: componentRows, totalErrors } = buildAnalyzeViewRows(filteredComponents, validatedComponents, allWarnings);
|
|
@@ -558,7 +564,7 @@ export function registerAnalyzeCommand(program) {
|
|
|
558
564
|
if (getInteractiveTerminalSupport().supported) {
|
|
559
565
|
const { waitUntilExit } = render(createElement(AnalyzeView, {
|
|
560
566
|
result: analyzeResult,
|
|
561
|
-
onExit: () =>
|
|
567
|
+
onExit: () => void exitWithAnalytics(0),
|
|
562
568
|
}));
|
|
563
569
|
await waitUntilExit();
|
|
564
570
|
}
|
|
@@ -585,7 +591,7 @@ export function registerAnalyzeCommand(program) {
|
|
|
585
591
|
summaryLines.push('Warnings: none');
|
|
586
592
|
}
|
|
587
593
|
process.stderr.write(summaryLines.join('\n') + '\n');
|
|
588
|
-
|
|
594
|
+
await exitWithAnalytics(0);
|
|
589
595
|
}
|
|
590
596
|
});
|
|
591
597
|
registerAnalyzeEditCommand(analyze);
|
|
@@ -8,6 +8,7 @@ import { loadReviewInput } from './parser.js';
|
|
|
8
8
|
import { App } from './tui/App.js';
|
|
9
9
|
import { openPipelineDb, loadRawComponents, storeRawComponents, createStep, updateStep } from '../../session/db.js';
|
|
10
10
|
import { validateExtractedComponents, shouldExcludeDueToValidation, formatExclusionWarning, } from '@contentful/experience-design-system-extraction';
|
|
11
|
+
import { bindAnalyticsSessionId, enrichCommandResult, exitWithAnalytics } from '../../analytics/index.js';
|
|
11
12
|
const SAFE_PATH_RE = /^[a-zA-Z0-9_.$[\]=]+$/;
|
|
12
13
|
const PROTO_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
13
14
|
function applyDotPath(obj, path, value) {
|
|
@@ -102,6 +103,7 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
|
|
|
102
103
|
}
|
|
103
104
|
const accepted = snapshot.components.filter((c) => c.status === 'accepted').length;
|
|
104
105
|
const rejected = snapshot.components.filter((c) => c.status === 'rejected').length + (names.length || 0);
|
|
106
|
+
enrichCommandResult({ accepted_component_count: accepted });
|
|
105
107
|
process.stderr.write(`Accepted: ${accepted} Rejected: ${rejected}\n`);
|
|
106
108
|
return;
|
|
107
109
|
}
|
|
@@ -130,7 +132,7 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
|
|
|
130
132
|
lines.push('');
|
|
131
133
|
lines.push('Re-run with --exclude-invalid to auto-reject these components, or run analyze select interactively to fix them.');
|
|
132
134
|
process.stderr.write(lines.join('\n') + '\n');
|
|
133
|
-
|
|
135
|
+
await exitWithAnalytics(1);
|
|
134
136
|
return;
|
|
135
137
|
}
|
|
136
138
|
}
|
|
@@ -175,14 +177,14 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
|
|
|
175
177
|
const parsed = JSON.parse(raw);
|
|
176
178
|
if (!Array.isArray(parsed)) {
|
|
177
179
|
process.stderr.write(`Error: --patch file must be a JSON array of patch operations: ${opts.patch}\n`);
|
|
178
|
-
|
|
180
|
+
await exitWithAnalytics(1);
|
|
179
181
|
return;
|
|
180
182
|
}
|
|
181
183
|
patchOps = parsed;
|
|
182
184
|
}
|
|
183
185
|
catch {
|
|
184
186
|
process.stderr.write(`Error: cannot read or parse --patch file: ${opts.patch}\n`);
|
|
185
|
-
|
|
187
|
+
await exitWithAnalytics(1);
|
|
186
188
|
return;
|
|
187
189
|
}
|
|
188
190
|
// Warn on unknown component names
|
|
@@ -229,6 +231,7 @@ async function runNonInteractive(snapshot, opts, paths, sessionId) {
|
|
|
229
231
|
db.close();
|
|
230
232
|
}
|
|
231
233
|
}
|
|
234
|
+
enrichCommandResult({ accepted_component_count: accepted.length });
|
|
232
235
|
process.stderr.write(`Accepted: ${accepted.length} Rejected: ${rejected.length}\n`);
|
|
233
236
|
}
|
|
234
237
|
/**
|
|
@@ -345,7 +348,7 @@ export async function rejectComponentsByName(sessionId, names, opts = {}) {
|
|
|
345
348
|
const components = snapshot.components.map((c) => (nameSet.has(c.name) ? { ...c, status: 'rejected' } : c));
|
|
346
349
|
await saveReviewState(paths.statePath, { ...snapshot, components });
|
|
347
350
|
}
|
|
348
|
-
function resolveSessionId(sessionFlag) {
|
|
351
|
+
async function resolveSessionId(sessionFlag) {
|
|
349
352
|
if (sessionFlag)
|
|
350
353
|
return sessionFlag;
|
|
351
354
|
const db = openPipelineDb();
|
|
@@ -360,7 +363,7 @@ function resolveSessionId(sessionFlag) {
|
|
|
360
363
|
.get();
|
|
361
364
|
if (!row) {
|
|
362
365
|
process.stderr.write('Error: no completed analyze extract session found. Run analyze extract first, or pass --session <id>.\n');
|
|
363
|
-
|
|
366
|
+
return await exitWithAnalytics(1);
|
|
364
367
|
}
|
|
365
368
|
return row.id;
|
|
366
369
|
}
|
|
@@ -384,7 +387,8 @@ export function registerAnalyzeEditCommand(program) {
|
|
|
384
387
|
.option('--exclude-invalid', 'With --select-all: auto-reject components with validation errors instead of failing loud (opt-in bypass for the gate)')
|
|
385
388
|
.option('--exclude-components <names>', 'Comma-separated component names to force-reject, regardless of other selection flags')
|
|
386
389
|
.action(async ({ session: sessionFlag, projectRoot, acceptAll, selectAll, reject, deselect, select, patch, excludeInvalid, excludeComponents, }) => {
|
|
387
|
-
const sessionId = resolveSessionId(sessionFlag);
|
|
390
|
+
const sessionId = await resolveSessionId(sessionFlag);
|
|
391
|
+
await bindAnalyticsSessionId(sessionId);
|
|
388
392
|
const db = openPipelineDb();
|
|
389
393
|
let rawComponentCount = 0;
|
|
390
394
|
try {
|
|
@@ -395,7 +399,7 @@ export function registerAnalyzeEditCommand(program) {
|
|
|
395
399
|
}
|
|
396
400
|
if (rawComponentCount === 0) {
|
|
397
401
|
process.stderr.write(`Error: session '${sessionId}' has no raw components. Run analyze extract first.\n`);
|
|
398
|
-
|
|
402
|
+
await exitWithAnalytics(1);
|
|
399
403
|
return;
|
|
400
404
|
}
|
|
401
405
|
const artifactsRoot = getRefineArtifactsRoot();
|
|
@@ -420,7 +424,7 @@ export function registerAnalyzeEditCommand(program) {
|
|
|
420
424
|
}
|
|
421
425
|
catch (error) {
|
|
422
426
|
process.stderr.write(`Error: unable to initialize refine session.\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
423
|
-
|
|
427
|
+
await exitWithAnalytics(1);
|
|
424
428
|
return;
|
|
425
429
|
}
|
|
426
430
|
// Non-interactive path
|
|
@@ -465,7 +469,7 @@ export function registerAnalyzeEditCommand(program) {
|
|
|
465
469
|
});
|
|
466
470
|
if (process.stdout.columns !== undefined && process.stdout.columns < 60) {
|
|
467
471
|
process.stderr.write(`Error: terminal too narrow (${process.stdout.columns} cols). Resize to 60+ columns.\n`);
|
|
468
|
-
|
|
472
|
+
await exitWithAnalytics(1);
|
|
469
473
|
}
|
|
470
474
|
const { waitUntilExit } = render(createElement(App, {
|
|
471
475
|
sessionId,
|