@sanity/workflow-cli 0.10.0 → 0.11.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/CHANGELOG.md +31 -0
- package/README.md +26 -18
- package/dist/commands/definition/list.js +2 -2
- package/dist/commands/definition/show.d.ts +13 -0
- package/dist/commands/definition/show.js +23 -5
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.js +4 -4
- package/dist/commands/diagnose.d.ts +1 -1
- package/dist/commands/diagnose.js +23 -13
- package/dist/commands/fire-action.js +13 -2
- package/dist/commands/list.d.ts +11 -2
- package/dist/commands/list.js +37 -16
- package/dist/commands/start.d.ts +1 -22
- package/dist/commands/start.js +1 -26
- package/dist/commands/tail.js +15 -12
- package/dist/hooks/prerun/telemetry.d.ts +4 -1
- package/dist/hooks/prerun/telemetry.js +6 -2
- package/dist/lib/definitions.d.ts +10 -0
- package/dist/lib/definitions.js +6 -0
- package/dist/lib/fail.d.ts +7 -0
- package/dist/lib/fail.js +2 -1
- package/dist/lib/share-definitions.d.ts +59 -25
- package/dist/lib/share-definitions.js +102 -21
- package/dist/lib/telemetry-setup.d.ts +4 -0
- package/dist/lib/telemetry-setup.js +18 -11
- package/dist/lib/telemetry.d.ts +33 -2
- package/dist/lib/telemetry.js +9 -4
- package/dist/lib/ui.js +0 -1
- package/oclif.manifest.json +18 -10
- package/package.json +4 -4
package/dist/commands/tail.js
CHANGED
|
@@ -31,7 +31,11 @@ export default class Tail extends WorkflowCommand {
|
|
|
31
31
|
this.log(styleText('dim', `tailing ${styleText('bold', args.instanceId)} (${initial.definition} v${initial.pinnedVersion}, ${initial.history.length} prior entries, currently in ${initial.currentStage})`));
|
|
32
32
|
this.log(styleText('dim', 'Ctrl+C to stop'));
|
|
33
33
|
this.log('');
|
|
34
|
-
let
|
|
34
|
+
let rejectListen;
|
|
35
|
+
const listenFailed = new Promise((_, reject) => {
|
|
36
|
+
rejectListen = reject;
|
|
37
|
+
});
|
|
38
|
+
let printChain = Promise.resolve(initial.history.length);
|
|
35
39
|
const subscription = client
|
|
36
40
|
.listen(`*[_id == $id]`, { id: args.instanceId }, {
|
|
37
41
|
includeResult: false,
|
|
@@ -39,17 +43,13 @@ export default class Tail extends WorkflowCommand {
|
|
|
39
43
|
tag: TAIL_TAG,
|
|
40
44
|
})
|
|
41
45
|
.subscribe({
|
|
42
|
-
next:
|
|
43
|
-
|
|
44
|
-
seen = await this.printNewEntries({ client, instanceId: args.instanceId, seen });
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
46
|
+
next: () => {
|
|
47
|
+
printChain = printChain.then((seen) => this.printNewEntries({ client, instanceId: args.instanceId, seen }).catch((err) => {
|
|
47
48
|
process.stderr.write(`${styleText('red', `${logSymbols.error} fetch failed:`)} ${errorMessage(err)}\n`);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
error: (err) => {
|
|
51
|
-
fail('Listen subscription error:', failureDetail(err));
|
|
49
|
+
return seen;
|
|
50
|
+
}));
|
|
52
51
|
},
|
|
52
|
+
error: rejectListen,
|
|
53
53
|
});
|
|
54
54
|
const shutdown = () => {
|
|
55
55
|
subscription.unsubscribe();
|
|
@@ -58,7 +58,10 @@ export default class Tail extends WorkflowCommand {
|
|
|
58
58
|
};
|
|
59
59
|
process.on('SIGINT', shutdown);
|
|
60
60
|
process.on('SIGTERM', shutdown);
|
|
61
|
-
await
|
|
61
|
+
await listenFailed.catch((err) => {
|
|
62
|
+
subscription.unsubscribe();
|
|
63
|
+
fail('Listen subscription error:', failureDetail(err));
|
|
64
|
+
});
|
|
62
65
|
}
|
|
63
66
|
async printNewEntries({ client, instanceId, seen, }) {
|
|
64
67
|
const next = await client.getDocument(instanceId, { tag: TAIL_TAG });
|
|
@@ -68,6 +71,6 @@ export default class Tail extends WorkflowCommand {
|
|
|
68
71
|
for (const entry of newEntries) {
|
|
69
72
|
this.log(`${styleText('dim', `[${formatTimestamp(entry.at)}]`)} ${styleText('cyan', displayTitle(entry._type))}`);
|
|
70
73
|
}
|
|
71
|
-
return next.history.length;
|
|
74
|
+
return Math.max(seen, next.history.length);
|
|
72
75
|
}
|
|
73
76
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Hook } from '@oclif/core';
|
|
2
2
|
/** Builds and installs the invocation's telemetry shell before the command
|
|
3
|
-
* runs; the `finally` hook completes the trace and flushes. Never throws.
|
|
3
|
+
* runs; the `finally` hook completes the trace and flushes. Never throws.
|
|
4
|
+
* An explicit `--share-defs` forces the store to send despite CI /
|
|
5
|
+
* `DO_NOT_TRACK` — resolved from raw argv here, since the store is built
|
|
6
|
+
* before oclif parses flags. */
|
|
4
7
|
declare const hook: Hook<'prerun'>;
|
|
5
8
|
export default hook;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { shouldForceShareTelemetry } from "../../lib/share-definitions.js";
|
|
1
2
|
import { setupCliTelemetry } from "../../lib/telemetry-setup.js";
|
|
2
|
-
const hook = async function ({ config }) {
|
|
3
|
-
await setupCliTelemetry({
|
|
3
|
+
const hook = async function ({ config, Command, argv }) {
|
|
4
|
+
await setupCliTelemetry({
|
|
5
|
+
cliVersion: config.version,
|
|
6
|
+
forceSend: shouldForceShareTelemetry({ commandId: Command?.id, argv }),
|
|
7
|
+
});
|
|
4
8
|
};
|
|
5
9
|
export default hook;
|
|
@@ -9,6 +9,16 @@ export declare function buildDefinitionShowQuery(flags: DefinitionShowQueryArgs)
|
|
|
9
9
|
groq: string;
|
|
10
10
|
params: Record<string, unknown>;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* The tag partitions holding any version of a definition name within one
|
|
14
|
+
* dataset. An untagged lookup that would span several partitions must error
|
|
15
|
+
* (the highest version ACROSS partitions is meaningless — it could show a
|
|
16
|
+
* prod operator the dev definition), the same way a cross-dataset hit does.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildDefinitionTagsQuery(name: string): {
|
|
19
|
+
groq: string;
|
|
20
|
+
params: Record<string, unknown>;
|
|
21
|
+
};
|
|
12
22
|
/**
|
|
13
23
|
* Fetch the deployed definition a command targets — latest unless pinned via
|
|
14
24
|
* `version`. An absent pinned version always fails (the caller asked for
|
package/dist/lib/definitions.js
CHANGED
|
@@ -14,6 +14,12 @@ export function buildDefinitionShowQuery(flags) {
|
|
|
14
14
|
const groq = `*[${filters.join(' && ')}] | order(version desc) [0]`;
|
|
15
15
|
return { groq, params };
|
|
16
16
|
}
|
|
17
|
+
export function buildDefinitionTagsQuery(name) {
|
|
18
|
+
return {
|
|
19
|
+
groq: `array::unique(*[_type == "${WORKFLOW_DEFINITION_TYPE}" && name == $name].tag)`,
|
|
20
|
+
params: { name },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
17
23
|
export async function fetchDeployedDefinition({ client, name, tag, version, }) {
|
|
18
24
|
const { groq, params } = buildDefinitionShowQuery({ name, tag, version });
|
|
19
25
|
const deployed = await client.fetch(groq, params, {
|
package/dist/lib/fail.d.ts
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
* stderr — no oclif framing, no Node stack — so expected failures
|
|
4
4
|
* (validation, missing input, conflicting flags) read clean.
|
|
5
5
|
*
|
|
6
|
+
* Exits by THROWING oclif's `ExitError`, never `process.exit`: the error
|
|
7
|
+
* rides the normal command lifecycle, so the `finally` hook still runs —
|
|
8
|
+
* completing the command trace and flushing batched telemetry — before
|
|
9
|
+
* oclif's handler exits 1 without printing anything further. A raw
|
|
10
|
+
* `process.exit` here would silently drop the trace and every batched
|
|
11
|
+
* event (including already-logged events from earlier in the command).
|
|
12
|
+
*
|
|
6
13
|
* For unexpected failures (network, engine throw) prefer letting the
|
|
7
14
|
* error propagate so the stack is visible for debugging.
|
|
8
15
|
*/
|
package/dist/lib/fail.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
+
import { Errors } from '@oclif/core';
|
|
2
3
|
import { ClientError } from '@sanity/client';
|
|
3
4
|
import { errorMessage } from '@sanity/workflow-engine';
|
|
4
5
|
import logSymbols from 'log-symbols';
|
|
@@ -10,7 +11,7 @@ export function fail(headline, detail) {
|
|
|
10
11
|
process.stderr.write(` ${styleText(['dim', 'red'], line)}\n`);
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
+
return Errors.exit(1);
|
|
14
15
|
}
|
|
15
16
|
export function failOnThrow(headline, fn) {
|
|
16
17
|
try {
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Definition sharing —
|
|
3
|
-
* telemetry consent. Sharing deployed workflow definitions with Sanity is
|
|
2
|
+
* Definition sharing — donating deployed workflow definitions to Sanity is
|
|
4
3
|
* CONTENT sharing, not telemetry: definitions carry customer-authored
|
|
5
4
|
* strings (names, titles, GROQ filters) that the telemetry payload policy
|
|
6
5
|
* bans, so the documents go to Sanity's first-party definition-feedback
|
|
7
|
-
* endpoint — one POST per deploy invocation — and telemetry carries only
|
|
8
|
-
*
|
|
6
|
+
* endpoint — one POST per deploy invocation — and telemetry carries only a
|
|
7
|
+
* content-free marker per shared definition plus a per-invocation decision
|
|
8
|
+
* event.
|
|
9
9
|
*
|
|
10
|
-
* Consent model:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* Consent model: opt-OUT. A deploy that creates new definition versions
|
|
11
|
+
* donates by default. The choice is skipped by an explicit flag:
|
|
12
|
+
* `--share-defs` (donate, no prompt) or `--no-share-defs` (don't). Without a
|
|
13
|
+
* flag, an interactive terminal takes a ONE-TIME acknowledgement (persisted
|
|
14
|
+
* in the shared Sanity user config, so it is asked once ever) before the
|
|
15
|
+
* first donation — notice must precede the send, since we cannot disclose
|
|
16
|
+
* taking content after taking it. Unattended runs (CI / `DO_NOT_TRACK` / a
|
|
17
|
+
* non-TTY pipe) can't prompt, so they donate nothing — the standard Sanity-CLI
|
|
18
|
+
* "silent in CI / `DO_NOT_TRACK`" rule; opt in per run with `--share-defs`.
|
|
16
19
|
*/
|
|
17
20
|
import { type DeployDefinitionResult, type DeployedDefinition, type WorkflowResource } from '@sanity/workflow-engine';
|
|
21
|
+
import type { UserConfigStore } from './telemetry-setup.ts';
|
|
18
22
|
import { type WorkflowDefinitionSharedData } from './telemetry.ts';
|
|
19
23
|
/** The client slice sharing uses — the read-back of just-deployed
|
|
20
24
|
* documents and the feedback POST, structurally, so a full
|
|
@@ -46,13 +50,22 @@ export interface ShareCandidate {
|
|
|
46
50
|
* routed project-agnostically through the API gateway). */
|
|
47
51
|
export declare const SHARE_ENDPOINT_URI = "/workflow/definition-feedback";
|
|
48
52
|
/**
|
|
49
|
-
* The
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
53
|
+
* The first-run consent prompt — a product contract, pinned by test. It must
|
|
54
|
+
* name the recipient (Sanity), state that the document ships VERBATIM with its
|
|
55
|
+
* deployment coordinates and is kept, name what is never shared, and disclose
|
|
56
|
+
* that the answer is remembered machine-wide (not just for this deploy) with
|
|
57
|
+
* the per-run override. Shown once (persisted) before the first donation on an
|
|
58
|
+
* interactive terminal; the answer decides and is remembered.
|
|
54
59
|
*/
|
|
55
|
-
export declare const
|
|
60
|
+
export declare const SHARE_CONSENT_PROMPT: string;
|
|
61
|
+
/**
|
|
62
|
+
* The one-line reminders printed on every flagless interactive deploy after the
|
|
63
|
+
* first — product contracts, pinned by test. They keep an ongoing donation (or
|
|
64
|
+
* a remembered decline) visible every run, never silent, and name the per-run
|
|
65
|
+
* override. `_ON` shows when the remembered answer shares, `_OFF` when it does not.
|
|
66
|
+
*/
|
|
67
|
+
export declare const SHARE_REMINDER_ON: string;
|
|
68
|
+
export declare const SHARE_REMINDER_OFF: string;
|
|
56
69
|
/**
|
|
57
70
|
* The content-free telemetry marker for one shared definition — projected
|
|
58
71
|
* from the engine's own deploy-event derivation ({@link definitionDeployedData})
|
|
@@ -66,21 +79,42 @@ export declare function definitionShareMarker(args: {
|
|
|
66
79
|
deployId: string;
|
|
67
80
|
}): WorkflowDefinitionSharedData;
|
|
68
81
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* (
|
|
74
|
-
*
|
|
75
|
-
*
|
|
82
|
+
* Whether the invocation's telemetry should send despite the environment
|
|
83
|
+
* denial (CI / `DO_NOT_TRACK`). Only an explicit `--share-defs` that actually
|
|
84
|
+
* donates forces it — a command-line arg beats env vars, and a `--share-defs`
|
|
85
|
+
* donation must never be recorded-less. Account-level consent is never
|
|
86
|
+
* overridden (the intake still reads `/intake/telemetry-status`).
|
|
87
|
+
*
|
|
88
|
+
* Computed from raw argv because the prerun hook builds the store before oclif
|
|
89
|
+
* parses flags, so it must land on the same value oclif will: repeated
|
|
90
|
+
* `allowNo` booleans are **last-token-wins**, and tokens after `--` aren't
|
|
91
|
+
* flags. `--dry-run` / `--check` never create versions, so they never donate —
|
|
92
|
+
* no telemetry to force. The `share-defs` flag has no char/alias/env source,
|
|
93
|
+
* so `--share-defs` / `--no-share-defs` are its only tokens (add one and this
|
|
94
|
+
* match must learn it too).
|
|
95
|
+
*/
|
|
96
|
+
export declare function shouldForceShareTelemetry(args: {
|
|
97
|
+
commandId: string | undefined;
|
|
98
|
+
argv: string[];
|
|
99
|
+
}): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* The whole post-deploy sharing flow. Resolves the opt-out decision (explicit
|
|
102
|
+
* flag, one-time acknowledgement, or standing policy), donates when it lands
|
|
103
|
+
* on share, and always records the content-free per-invocation decision so
|
|
104
|
+
* adoption dashboards can measure opt-out rate. No newly created versions
|
|
105
|
+
* (unchanged re-deploys, dry runs, nothing succeeded) means no decision, no
|
|
106
|
+
* prompt, and no POST.
|
|
76
107
|
*
|
|
77
|
-
* The optional args exist for the
|
|
108
|
+
* The optional args exist for the flow's dependencies, mirroring
|
|
78
109
|
* `setupCliTelemetry` — omitted, the real process surfaces apply.
|
|
79
110
|
*/
|
|
80
111
|
export declare function shareDefinitionsAfterDeploy(args: {
|
|
81
|
-
flag: boolean;
|
|
112
|
+
flag: boolean | undefined;
|
|
82
113
|
candidates: ShareCandidate[];
|
|
83
114
|
warn: (message: string) => void;
|
|
84
115
|
interactive?: boolean;
|
|
116
|
+
envDenied?: boolean;
|
|
85
117
|
writeStderr?: (message: string) => void;
|
|
118
|
+
userConfig?: UserConfigStore;
|
|
119
|
+
confirmShare?: () => Promise<boolean>;
|
|
86
120
|
}): Promise<void>;
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
|
-
import { isInteractive } from '@sanity/cli-core';
|
|
3
|
-
import {
|
|
2
|
+
import { getUserConfig, isInteractive } from '@sanity/cli-core';
|
|
3
|
+
import { confirm } from '@sanity/cli-core/ux';
|
|
4
|
+
import { definitionDeployedData, errorMessage, isTelemetryEnvDenied, } from '@sanity/workflow-engine';
|
|
4
5
|
import { buildDefinitionShowQuery } from "./definitions.js";
|
|
5
|
-
import { cliTelemetry, WorkflowDefinitionShared, } from "./telemetry.js";
|
|
6
|
+
import { cliTelemetry, WorkflowDefinitionShared, WorkflowDefinitionSharingDecided, } from "./telemetry.js";
|
|
6
7
|
export const SHARE_ENDPOINT_URI = '/workflow/definition-feedback';
|
|
7
8
|
const SHARE_TAG = 'definition.share';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'
|
|
11
|
-
'
|
|
9
|
+
const SHARE_DECISION_KEY = 'workflowCliDefinitionSharing';
|
|
10
|
+
export const SHARE_CONSENT_PROMPT = `${styleText('bold', 'Share the workflow definitions this deploy creates with Sanity?')}\n` +
|
|
11
|
+
'They are sent verbatim (structure, names, filters, effect configuration,\n' +
|
|
12
|
+
'seeded values) with their deployment coordinates, and kept to improve\n' +
|
|
13
|
+
'Editorial Workflows — never your content documents, workflow instances, or\n' +
|
|
14
|
+
'your Sanity auth token. Your answer is remembered for future deploys on\n' +
|
|
15
|
+
`this machine; override any run with ${styleText('cyan', '--share-defs')} / ${styleText('cyan', '--no-share-defs')}.`;
|
|
16
|
+
export const SHARE_REMINDER_ON = `${styleText('bold', 'Sharing new workflow definitions with Sanity')} to improve Editorial Workflows` +
|
|
17
|
+
` — opt out with ${styleText('cyan', '--no-share-defs')}.\n`;
|
|
18
|
+
export const SHARE_REMINDER_OFF = `Not sharing workflow definitions with Sanity — share with ${styleText('cyan', '--share-defs')}.\n`;
|
|
12
19
|
export function definitionShareMarker(args) {
|
|
13
20
|
const { status: _status, ...structural } = definitionDeployedData(args.definition, {
|
|
14
21
|
contentHash: args.contentHash,
|
|
@@ -17,6 +24,18 @@ export function definitionShareMarker(args) {
|
|
|
17
24
|
});
|
|
18
25
|
return structural;
|
|
19
26
|
}
|
|
27
|
+
export function shouldForceShareTelemetry(args) {
|
|
28
|
+
const { commandId, argv } = args;
|
|
29
|
+
if (commandId !== 'deploy') {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const terminator = argv.indexOf('--');
|
|
33
|
+
const tokens = terminator === -1 ? argv : argv.slice(0, terminator);
|
|
34
|
+
if (tokens.includes('--dry-run') || tokens.includes('--check')) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return tokens.lastIndexOf('--share-defs') > tokens.lastIndexOf('--no-share-defs');
|
|
38
|
+
}
|
|
20
39
|
async function loadShareEntry(args) {
|
|
21
40
|
const { candidate, result, warn } = args;
|
|
22
41
|
const deployed = await loadDeployedForShare(args);
|
|
@@ -58,31 +77,60 @@ async function loadDeployedForShare(args) {
|
|
|
58
77
|
return undefined;
|
|
59
78
|
}
|
|
60
79
|
}
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
function collectCreated(candidates) {
|
|
81
|
+
return candidates.flatMap((candidate) => candidate.results
|
|
63
82
|
.filter((result) => result.status === 'created')
|
|
64
83
|
.map((result) => ({ candidate, result })));
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
}
|
|
85
|
+
function readShareDecision(userConfig) {
|
|
86
|
+
const value = userConfig.get(SHARE_DECISION_KEY);
|
|
87
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
88
|
+
}
|
|
89
|
+
async function promptFirstRun(args) {
|
|
90
|
+
let share;
|
|
91
|
+
try {
|
|
92
|
+
share = await args.confirmShare();
|
|
67
93
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
args.userConfig.set(SHARE_DECISION_KEY, share);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
}
|
|
102
|
+
return share;
|
|
103
|
+
}
|
|
104
|
+
async function resolveShareIntent(args) {
|
|
105
|
+
if (args.flag === true) {
|
|
106
|
+
return { share: true, decision: 'opt-in' };
|
|
107
|
+
}
|
|
108
|
+
if (args.flag === false) {
|
|
109
|
+
return { share: false, decision: 'opt-out' };
|
|
110
|
+
}
|
|
111
|
+
const unattended = args.envDenied || !args.interactive;
|
|
112
|
+
if (unattended) {
|
|
113
|
+
return { share: false, decision: 'unattended' };
|
|
75
114
|
}
|
|
115
|
+
const remembered = readShareDecision(args.userConfig);
|
|
116
|
+
if (remembered !== undefined) {
|
|
117
|
+
args.writeStderr(remembered ? SHARE_REMINDER_ON : SHARE_REMINDER_OFF);
|
|
118
|
+
return { share: remembered, decision: remembered ? 'remembered-share' : 'remembered-decline' };
|
|
119
|
+
}
|
|
120
|
+
const accepted = await promptFirstRun(args);
|
|
121
|
+
return { share: accepted, decision: accepted ? 'prompt-accepted' : 'prompt-declined' };
|
|
122
|
+
}
|
|
123
|
+
async function donate(args) {
|
|
76
124
|
const shared = [];
|
|
77
125
|
try {
|
|
78
|
-
for (const item of created) {
|
|
126
|
+
for (const item of args.created) {
|
|
79
127
|
const entry = await loadShareEntry({ ...item, warn: args.warn });
|
|
80
128
|
if (entry !== undefined) {
|
|
81
129
|
shared.push({ entry, candidate: item.candidate });
|
|
82
130
|
}
|
|
83
131
|
}
|
|
84
132
|
if (shared.length === 0) {
|
|
85
|
-
return;
|
|
133
|
+
return false;
|
|
86
134
|
}
|
|
87
135
|
await shared[0].candidate.client.request({
|
|
88
136
|
uri: SHARE_ENDPOINT_URI,
|
|
@@ -93,8 +141,12 @@ export async function shareDefinitionsAfterDeploy(args) {
|
|
|
93
141
|
}
|
|
94
142
|
catch (error) {
|
|
95
143
|
args.warn(`Definition sharing failed — nothing shared. ${errorMessage(error)}`);
|
|
96
|
-
return;
|
|
144
|
+
return false;
|
|
97
145
|
}
|
|
146
|
+
emitShareMarkers(shared);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
function emitShareMarkers(shared) {
|
|
98
150
|
try {
|
|
99
151
|
const { logger } = cliTelemetry();
|
|
100
152
|
for (const { entry, candidate } of shared) {
|
|
@@ -104,3 +156,32 @@ export async function shareDefinitionsAfterDeploy(args) {
|
|
|
104
156
|
catch {
|
|
105
157
|
}
|
|
106
158
|
}
|
|
159
|
+
function emitSharingDecided(data) {
|
|
160
|
+
try {
|
|
161
|
+
cliTelemetry().logger.log(WorkflowDefinitionSharingDecided, data);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export async function shareDefinitionsAfterDeploy(args) {
|
|
167
|
+
const created = collectCreated(args.candidates);
|
|
168
|
+
if (created.length === 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const { share, decision } = await resolveShareIntent({
|
|
173
|
+
flag: args.flag,
|
|
174
|
+
interactive: args.interactive ?? (isInteractive() && process.stderr.isTTY === true),
|
|
175
|
+
envDenied: args.envDenied ?? isTelemetryEnvDenied(process.env),
|
|
176
|
+
userConfig: args.userConfig ?? getUserConfig(),
|
|
177
|
+
writeStderr: args.writeStderr ?? ((message) => void process.stderr.write(message)),
|
|
178
|
+
confirmShare: args.confirmShare ??
|
|
179
|
+
(() => confirm({ message: SHARE_CONSENT_PROMPT, default: true }, { output: process.stderr })),
|
|
180
|
+
});
|
|
181
|
+
const shared = share ? await donate({ created, warn: args.warn }) : false;
|
|
182
|
+
emitSharingDecided({ decision, shared, definitionCount: created.length });
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
args.warn(`Definition sharing skipped — ${errorMessage(error)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -39,6 +39,10 @@ export declare function setupCliTelemetry(args?: {
|
|
|
39
39
|
writeStderr?: (message: string) => void;
|
|
40
40
|
/** The oclif config's version — the prerun hook supplies it. */
|
|
41
41
|
cliVersion?: string;
|
|
42
|
+
/** Override the environment denial (CI / `DO_NOT_TRACK`) — the prerun hook
|
|
43
|
+
* sets this for a deploy that explicitly opts into definition sharing with
|
|
44
|
+
* `--share-defs`. */
|
|
45
|
+
forceSend?: boolean;
|
|
42
46
|
/** Intake client override — tests inject a fake here, like the other
|
|
43
47
|
* hook dependencies above; omitted, the real project client applies. */
|
|
44
48
|
client?: TelemetryIntakeClient;
|
|
@@ -49,23 +49,30 @@ export async function setupCliTelemetry(args) {
|
|
|
49
49
|
client,
|
|
50
50
|
projectId: project.projectId,
|
|
51
51
|
env: args?.env ?? process.env,
|
|
52
|
+
forceSend: args?.forceSend ?? false,
|
|
52
53
|
});
|
|
53
54
|
setCliTelemetry(telemetry);
|
|
54
|
-
|
|
55
|
-
telemetry.store.logger.updateUserProperties(cliUserProperties({ project, cliVersion: args?.cliVersion }));
|
|
56
|
-
void resolveOrgId(client, project.projectId).then((orgId) => {
|
|
57
|
-
if (orgId !== undefined)
|
|
58
|
-
telemetry.store.logger.updateUserProperties({ orgId });
|
|
59
|
-
});
|
|
60
|
-
maybeShowTelemetryDisclosure({
|
|
61
|
-
userConfig: args?.userConfig ?? getUserConfig(),
|
|
62
|
-
writeStderr: args?.writeStderr ?? ((message) => void process.stderr.write(message)),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
55
|
+
attachBuiltinContext({ telemetry, project, client, deps: args });
|
|
65
56
|
}
|
|
66
57
|
catch {
|
|
67
58
|
}
|
|
68
59
|
}
|
|
60
|
+
function attachBuiltinContext(args) {
|
|
61
|
+
const { telemetry, project, client, deps } = args;
|
|
62
|
+
if (!telemetry.envDenied || telemetry.forceSend) {
|
|
63
|
+
telemetry.store.logger.updateUserProperties(cliUserProperties({ project, cliVersion: deps?.cliVersion }));
|
|
64
|
+
void resolveOrgId(client, project.projectId).then((orgId) => {
|
|
65
|
+
if (orgId !== undefined)
|
|
66
|
+
telemetry.store.logger.updateUserProperties({ orgId });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (!telemetry.envDenied) {
|
|
70
|
+
maybeShowTelemetryDisclosure({
|
|
71
|
+
userConfig: deps?.userConfig ?? getUserConfig(),
|
|
72
|
+
writeStderr: deps?.writeStderr ?? ((message) => void process.stderr.write(message)),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
69
76
|
const ORG_LOOKUP_DEADLINE_MS = 2_000;
|
|
70
77
|
const CONTEXT_TAG = 'telemetry.context';
|
|
71
78
|
export function cliUserProperties(args) {
|
package/dist/lib/telemetry.d.ts
CHANGED
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
* never flag values, arguments, or error text. `Editorial Workflows
|
|
24
24
|
* Definition Shared` is a content-free marker (hash + structural counts); the shared
|
|
25
25
|
* document itself rides the first-party feedback endpoint, never
|
|
26
|
-
* telemetry (see `share-definitions.ts`).
|
|
26
|
+
* telemetry (see `share-definitions.ts`). `Editorial Workflows Definition
|
|
27
|
+
* Sharing Decided` records the per-invocation consent outcome (default /
|
|
28
|
+
* opt-in / opt-out and whether anything was donated) — also content-free.
|
|
27
29
|
*/
|
|
28
30
|
import { type DefinedTelemetryTrace, type TelemetryStore } from '@sanity/telemetry';
|
|
29
31
|
import { type TelemetryIntakeClient, type WorkflowDefinitionDeployedData, type WorkflowTelemetryLogger } from '@sanity/workflow-engine';
|
|
@@ -42,6 +44,26 @@ export declare const WorkflowCliCommandExecuted: DefinedTelemetryTrace<WorkflowC
|
|
|
42
44
|
* are ever shared, so status carries nothing. */
|
|
43
45
|
export type WorkflowDefinitionSharedData = Omit<WorkflowDefinitionDeployedData, 'status'>;
|
|
44
46
|
export declare const WorkflowDefinitionShared: import("@sanity/telemetry").DefinedTelemetryLog<WorkflowDefinitionSharedData>;
|
|
47
|
+
/**
|
|
48
|
+
* How the donation decision was reached, and what it was. Paired with `shared`
|
|
49
|
+
* (whether it actually reached the endpoint) this keeps every case distinct —
|
|
50
|
+
* an explicit decline vs a run that couldn't ask, and a chosen share vs a
|
|
51
|
+
* failed POST:
|
|
52
|
+
* - `opt-in` / `opt-out` — an explicit `--share-defs` / `--no-share-defs`.
|
|
53
|
+
* - `prompt-accepted` / `prompt-declined` — the first-run consent prompt.
|
|
54
|
+
* - `remembered-share` / `remembered-decline` — a persisted earlier answer.
|
|
55
|
+
* - `unattended` — CI / `DO_NOT_TRACK` / non-TTY: never asked, never shares.
|
|
56
|
+
*/
|
|
57
|
+
export type DefinitionSharingDecision = 'opt-in' | 'opt-out' | 'prompt-accepted' | 'prompt-declined' | 'remembered-share' | 'remembered-decline' | 'unattended';
|
|
58
|
+
export interface WorkflowDefinitionSharingDecidedData {
|
|
59
|
+
decision: DefinitionSharingDecision;
|
|
60
|
+
/** Whether the donation actually reached the feedback endpoint — `false` for
|
|
61
|
+
* any non-sharing decision, and also for a chosen share whose POST failed. */
|
|
62
|
+
shared: boolean;
|
|
63
|
+
/** Newly created definition versions this invocation could have donated. */
|
|
64
|
+
definitionCount: number;
|
|
65
|
+
}
|
|
66
|
+
export declare const WorkflowDefinitionSharingDecided: import("@sanity/telemetry").DefinedTelemetryLog<WorkflowDefinitionSharingDecidedData>;
|
|
45
67
|
/**
|
|
46
68
|
* The shell for one CLI invocation. `builtin` is the Sanity-intake store;
|
|
47
69
|
* `provided` is a config-supplied logger that replaces the shell and its
|
|
@@ -53,6 +75,10 @@ export type CliTelemetry = {
|
|
|
53
75
|
logger: WorkflowTelemetryLogger;
|
|
54
76
|
store: TelemetryStore<unknown>;
|
|
55
77
|
envDenied: boolean;
|
|
78
|
+
/** The environment denial was overridden ({@link createBuiltinTelemetry}):
|
|
79
|
+
* the store sends despite CI / `DO_NOT_TRACK`, subject only to
|
|
80
|
+
* account consent. */
|
|
81
|
+
forceSend: boolean;
|
|
56
82
|
} | {
|
|
57
83
|
kind: 'provided';
|
|
58
84
|
logger: WorkflowTelemetryLogger;
|
|
@@ -66,11 +92,16 @@ export declare function cliTelemetry(): CliTelemetry;
|
|
|
66
92
|
export declare function setCliTelemetry(telemetry: CliTelemetry): void;
|
|
67
93
|
export declare function clearCliTelemetry(): void;
|
|
68
94
|
/** Build the built-in Sanity-intake shell over an authenticated,
|
|
69
|
-
* project-bound client.
|
|
95
|
+
* project-bound client. `forceSend` overrides the environment denial
|
|
96
|
+
* (CI / `DO_NOT_TRACK`) — an explicit `--share-defs` opts the deploy
|
|
97
|
+
* invocation's telemetry in regardless (a command-line arg beats env
|
|
98
|
+
* vars). Account-level consent is unaffected: the intake still fetches
|
|
99
|
+
* `/intake/telemetry-status`, so `sanity telemetry disable` still mutes. */
|
|
70
100
|
export declare function createBuiltinTelemetry(args: {
|
|
71
101
|
client: TelemetryIntakeClient;
|
|
72
102
|
projectId: string;
|
|
73
103
|
env: Record<string, string | undefined>;
|
|
104
|
+
forceSend?: boolean;
|
|
74
105
|
}): Extract<CliTelemetry, {
|
|
75
106
|
kind: 'builtin';
|
|
76
107
|
}>;
|
package/dist/lib/telemetry.js
CHANGED
|
@@ -8,7 +8,12 @@ export const WorkflowCliCommandExecuted = defineTrace({
|
|
|
8
8
|
export const WorkflowDefinitionShared = defineEvent({
|
|
9
9
|
name: 'Editorial Workflows Definition Shared',
|
|
10
10
|
version: 1,
|
|
11
|
-
description: 'A newly created workflow definition version was donated
|
|
11
|
+
description: 'A newly created workflow definition version was donated to Sanity. Content-free marker: the content hash plus the same structural counts Editorial Workflows Definition Deployed carries — the document itself goes to the first-party definition-feedback endpoint, never through telemetry.',
|
|
12
|
+
});
|
|
13
|
+
export const WorkflowDefinitionSharingDecided = defineEvent({
|
|
14
|
+
name: 'Editorial Workflows Definition Sharing Decided',
|
|
15
|
+
version: 1,
|
|
16
|
+
description: 'One per deploy invocation that created new definition versions: how the donation decision was reached and what it was (explicit flag / prompt accept-or-decline / remembered / unattended), whether anything reached the endpoint, and how many versions were eligible. Content-free — lets adoption dashboards separate explicit opt-outs, prompt declines, and unattended runs, and measure the true opt-out rate, without ever carrying the document.',
|
|
12
17
|
});
|
|
13
18
|
const OFF = { kind: 'off', logger: noopTelemetry };
|
|
14
19
|
let current = OFF;
|
|
@@ -28,10 +33,10 @@ export function clearCliTelemetry() {
|
|
|
28
33
|
activeTrace = undefined;
|
|
29
34
|
}
|
|
30
35
|
export function createBuiltinTelemetry(args) {
|
|
31
|
-
const { client, projectId, env } = args;
|
|
36
|
+
const { client, projectId, env, forceSend = false } = args;
|
|
32
37
|
const envDenied = isTelemetryEnvDenied(env);
|
|
33
|
-
const store = createBatchedStore(createSessionId(), createTelemetryIntake({ client, projectId, denied: envDenied }));
|
|
34
|
-
return { kind: 'builtin', logger: store.logger, store, envDenied };
|
|
38
|
+
const store = createBatchedStore(createSessionId(), createTelemetryIntake({ client, projectId, denied: forceSend ? false : envDenied }));
|
|
39
|
+
return { kind: 'builtin', logger: store.logger, store, envDenied, forceSend };
|
|
35
40
|
}
|
|
36
41
|
function traceAsLogEvent(trace) {
|
|
37
42
|
const { name, version, description } = trace;
|
package/dist/lib/ui.js
CHANGED