@dev.fast/whiteboard 0.0.0-stage → 0.2.1-preview.20261005.98
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/LICENSE +21 -0
- package/README.md +146 -2
- package/THIRD_PARTY_NOTICES.md +93 -0
- package/dist/account-alias-DOHN1RSH.js +973 -0
- package/dist/agent-cli-ChOq0ZuO.js +117 -0
- package/dist/agent-cli-DzWfZr6G.js +2 -0
- package/dist/agent-client-CPQo7iTI.js +305 -0
- package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
- package/dist/build-info.json +1 -0
- package/dist/cli-hVgHcdsv.js +47 -0
- package/dist/cli-runner-D8y2_luU.js +5095 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +128 -0
- package/dist/client-DFoh50W3.js +950 -0
- package/dist/desktop-discovery-DBO-PM2V.js +164 -0
- package/dist/error-message-OtiDonty.js +6 -0
- package/dist/fs-utils-BMPLt0cr.js +22 -0
- package/dist/fuzzy-match-BoAmcyak.js +30 -0
- package/dist/headless-host-BJxf4dUU.js +94 -0
- package/dist/input-error-OLgB_h31.js +11 -0
- package/dist/local-data-CxLk75rx.js +4491 -0
- package/dist/mcp-QeF8vkdf.js +140 -0
- package/dist/package-paths-B6-zxvIO.js +47 -0
- package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
- package/dist/profile-4ry2f3AM.js +76 -0
- package/dist/profile-Bh5FIrmf.js +2 -0
- package/dist/request-origin-D_4QIdoN.js +811 -0
- package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
- package/dist/review-home-paths-6zZH-1c9.js +2560 -0
- package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +2 -0
- package/dist/s3-SzLpeCG6.js +345 -0
- package/dist/s3-config-BgxkSoOy.js +2 -0
- package/dist/s3-config-CAxhO9u_.js +676 -0
- package/dist/server/desktop-host.d.ts +4 -0
- package/dist/server/desktop-host.js +1702 -0
- package/dist/server-discovery-Dbzn3w6Z.js +66 -0
- package/dist/sharing/index.d.ts +1977 -0
- package/dist/sharing/index.js +2 -0
- package/dist/src-CGP5ytbV.js +283 -0
- package/dist/src-CmdiBL20.js +3676 -0
- package/dist/src-DnwdaQ2r.js +865 -0
- package/dist/src-X9phtB2j.js +68 -0
- package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
- package/dist/tool-failure-C8zB73HV.js +98 -0
- package/dist/tutorial-trace-DrBvkaeL.js +43 -0
- package/instructions/authoring.md +36 -0
- package/instructions/file-lenses.md +13 -0
- package/instructions/scratchpad.md +23 -0
- package/instructions/trace-archaeology.md +113 -0
- package/onboarding.md +8 -0
- package/package.json +106 -3
- package/src/agent-selection.ts +100 -0
- package/src/agent-session-ref.ts +80 -0
- package/src/ask/agents.ts +351 -0
- package/src/ask/checkout-files.ts +47 -0
- package/src/ask/file-refs.ts +111 -0
- package/src/ask/pi-mcp.ts +154 -0
- package/src/ask/protocol.ts +244 -0
- package/src/ask/thread-state.ts +350 -0
- package/src/ask/thread.ts +1400 -0
- package/src/ask/threads.ts +219 -0
- package/src/ask/watch.ts +79 -0
- package/src/cli-install.ts +1041 -0
- package/src/cli-runner.ts +1461 -0
- package/src/cli-runtime-info.ts +35 -0
- package/src/cli.ts +222 -0
- package/src/connect-prompts.ts +248 -0
- package/src/cursor-deeplink.ts +11 -0
- package/src/desktop-discovery.ts +355 -0
- package/src/diff-selection-migration.ts +63 -0
- package/src/embedded-posthog-key.ts +6 -0
- package/src/error-telemetry.ts +247 -0
- package/src/evidence.ts +14 -0
- package/src/exception-telemetry.ts +126 -0
- package/src/fixtures/blocks/call_stack_diff.json +27 -0
- package/src/fixtures/blocks/callout.json +11 -0
- package/src/fixtures/blocks/code.json +9 -0
- package/src/fixtures/blocks/code_peek.json +8 -0
- package/src/fixtures/blocks/database_lens.json +51 -0
- package/src/fixtures/blocks/divider.json +1 -0
- package/src/fixtures/blocks/fixtures.ts +33 -0
- package/src/fixtures/blocks/flow_diagram.json +36 -0
- package/src/fixtures/blocks/ids.ts +8 -0
- package/src/fixtures/blocks/image.json +9 -0
- package/src/fixtures/blocks/markdown.json +7 -0
- package/src/fixtures/blocks/section.json +11 -0
- package/src/fixtures/blocks/sequence.json +31 -0
- package/src/fixtures/blocks/software_map.json +7 -0
- package/src/fixtures/blocks/trace_quote.json +9 -0
- package/src/fixtures/blocks/tutorial.json +51 -0
- package/src/fs-utils.ts +32 -0
- package/src/fuzzy-match.ts +94 -0
- package/src/install.ts +29 -0
- package/src/legacy-skills.ts +133 -0
- package/src/lens-selection.ts +230 -0
- package/src/markdown-latex-math.ts +230 -0
- package/src/markdown.ts +81 -0
- package/src/package-paths.ts +53 -0
- package/src/posthog-capture-client.ts +610 -0
- package/src/review-api/README.md +295 -0
- package/src/review-api/activity.ts +340 -0
- package/src/review-api/agent-cli.ts +219 -0
- package/src/review-api/agent-client.ts +188 -0
- package/src/review-api/anchor-quotes.ts +88 -0
- package/src/review-api/ask-history.ts +242 -0
- package/src/review-api/authoring-tools.ts +211 -0
- package/src/review-api/blocks/call_stack_diff.ts +75 -0
- package/src/review-api/blocks/callout.ts +24 -0
- package/src/review-api/blocks/code.ts +9 -0
- package/src/review-api/blocks/code_peek.ts +13 -0
- package/src/review-api/blocks/database_lens.ts +150 -0
- package/src/review-api/blocks/definition.ts +40 -0
- package/src/review-api/blocks/divider.ts +6 -0
- package/src/review-api/blocks/flow_diagram.ts +111 -0
- package/src/review-api/blocks/image.ts +10 -0
- package/src/review-api/blocks/index.ts +90 -0
- package/src/review-api/blocks/markdown.ts +17 -0
- package/src/review-api/blocks/section.ts +24 -0
- package/src/review-api/blocks/sequence.ts +56 -0
- package/src/review-api/blocks/software_map.ts +9 -0
- package/src/review-api/blocks/trace_quote.ts +10 -0
- package/src/review-api/blocks/tutorial.ts +39 -0
- package/src/review-api/checkout-fs.ts +14 -0
- package/src/review-api/client.ts +1 -0
- package/src/review-api/comparison-coverage.ts +304 -0
- package/src/review-api/component-reference.ts +30 -0
- package/src/review-api/diff-lenses.ts +175 -0
- package/src/review-api/document-headings.ts +51 -0
- package/src/review-api/document-text.ts +200 -0
- package/src/review-api/document.ts +906 -0
- package/src/review-api/file-lenses.ts +100 -0
- package/src/review-api/http.ts +1877 -0
- package/src/review-api/image-decode.ts +29 -0
- package/src/review-api/input-error.ts +9 -0
- package/src/review-api/instructions.ts +89 -0
- package/src/review-api/lens-alignment.ts +52 -0
- package/src/review-api/local-data.ts +1837 -0
- package/src/review-api/map-input.ts +154 -0
- package/src/review-api/mcp-client-agent.ts +32 -0
- package/src/review-api/mcp.ts +240 -0
- package/src/review-api/origin.ts +43 -0
- package/src/review-api/profile.ts +146 -0
- package/src/review-api/public-tools.ts +102 -0
- package/src/review-api/pull-request.ts +389 -0
- package/src/review-api/read-schemas.ts +85 -0
- package/src/review-api/recovery.ts +2 -0
- package/src/review-api/request-origin.ts +33 -0
- package/src/review-api/review-progress.ts +382 -0
- package/src/review-api/status-tool.ts +10 -0
- package/src/review-api/store-schema.ts +22 -0
- package/src/review-api/store.ts +1647 -0
- package/src/review-api/tool-failure.ts +64 -0
- package/src/review-api/trace-schema.ts +13 -0
- package/src/review-api/traces.ts +119 -0
- package/src/review-api/unsupported-files.integration.ts +91 -0
- package/src/review-api/workspaces.ts +692 -0
- package/src/review-api/worktree-source.ts +170 -0
- package/src/review-api/worktree-structural.integration.ts +320 -0
- package/src/review-app-launcher.ts +432 -0
- package/src/review-app-picker.ts +168 -0
- package/src/review-app.ts +134 -0
- package/src/review-bundled-tools.ts +249 -0
- package/src/review-checkout-paths.ts +37 -0
- package/src/review-diff-files.ts +89 -0
- package/src/review-head-checkout.ts +300 -0
- package/src/review-home-paths.ts +73 -0
- package/src/review-info.ts +59 -0
- package/src/review-instances.ts +113 -0
- package/src/review-logger.ts +183 -0
- package/src/review-preferences.ts +88 -0
- package/src/review-prepare.ts +291 -0
- package/src/review-stack.ts +112 -0
- package/src/review-telemetry.ts +1066 -0
- package/src/runtime.ts +74 -0
- package/src/server/account-alias.ts +30 -0
- package/src/server/bounded-stream.ts +34 -0
- package/src/server/bug-report.ts +301 -0
- package/src/server/client-error-budget.ts +45 -0
- package/src/server/crash-report.ts +273 -0
- package/src/server/desktop-host-shutdown.ts +64 -0
- package/src/server/desktop-host.ts +190 -0
- package/src/server/desktop-server.ts +703 -0
- package/src/server/diffr-config.ts +509 -0
- package/src/server/diffr-languages.ts +97 -0
- package/src/server/global-verb-relay.ts +190 -0
- package/src/server/headless-host.ts +149 -0
- package/src/server/hono-http.ts +162 -0
- package/src/server/http-json.ts +24 -0
- package/src/server/json-review-reporting.ts +174 -0
- package/src/server/process-error-telemetry.ts +174 -0
- package/src/server/review-api-parsers.ts +90 -0
- package/src/server/review-lifecycle-telemetry.ts +73 -0
- package/src/server/review-open-watchdog.ts +46 -0
- package/src/server/review-server-core.ts +290 -0
- package/src/server/structural-comparisons.ts +153 -0
- package/src/server/structural-diff.ts +186 -0
- package/src/server/tutorial-service.ts +241 -0
- package/src/server/ui-telemetry.ts +198 -0
- package/src/server-discovery.ts +95 -0
- package/src/session-markers.ts +132 -0
- package/src/sharing/auth.ts +41 -0
- package/src/sharing/cli.ts +83 -0
- package/src/sharing/client.ts +334 -0
- package/src/sharing/export.ts +195 -0
- package/src/sharing/host.ts +316 -0
- package/src/sharing/import.ts +787 -0
- package/src/sharing/index.ts +16 -0
- package/src/sharing/repository.ts +145 -0
- package/src/sharing/routes.ts +34 -0
- package/src/slug.ts +23 -0
- package/src/software-map-diff-counts.ts +517 -0
- package/src/software-map-model.ts +1147 -0
- package/src/software-map-topology-diff.ts +260 -0
- package/src/source.ts +92 -0
- package/src/startup-trace.ts +232 -0
- package/src/stored-document-migration.ts +151 -0
- package/src/telemetry-clean-text.ts +257 -0
- package/src/telemetry-config.ts +298 -0
- package/src/telemetry-debug-sink.ts +38 -0
- package/src/telemetry.ts +13 -0
- package/src/trace-cli.ts +156 -0
- package/src/trace-storage-cli.ts +509 -0
- package/src/tutorial-conversation.ts +18 -0
- package/src/ui-telemetry-events.ts +765 -0
- package/src/unified-diff.ts +71 -0
- package/src/viewed-coverage.ts +259 -0
- package/src/windows-cli.ts +125 -0
- package/tutorial/document.json +275 -0
- package/tutorial/runtime-manifest.json +12 -0
- package/tutorial/sample-service/package.json +9 -0
- package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
- package/tutorial/sample-service/src/app.ts +26 -0
- package/tutorial/sample-service/src/database/schema.ts +10 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
- package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
- package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
- package/tutorial/sample-service/src/orders/order-service.ts +34 -0
- package/tutorial/sample-service/src/orders/order.ts +21 -0
- package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
- package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
- package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
- package/tutorial/sample-service/tsconfig.json +12 -0
- package/tutorial/software-map.json +139 -0
- package/tutorial/trace.json +20 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { type JsonValue, isJsonObject } from "@dev.fast/review-protocol";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { migrateDiffSelections } from "./diff-selection-migration.js";
|
|
5
|
+
import { formatAnchor } from "./lens-selection.js";
|
|
6
|
+
import { label } from "./review-api/blocks/definition.js";
|
|
7
|
+
import { type Lens, lensTargetsSchema } from "./review-api/diff-lenses.js";
|
|
8
|
+
|
|
9
|
+
/** Upgrade a saved or shared Review document to the current block schema at the
|
|
10
|
+
* read/import boundary. New edits use the strict schema and never accept these
|
|
11
|
+
* retired forms. */
|
|
12
|
+
// This is the decoder boundary for stored documents in retired wire formats.
|
|
13
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters
|
|
14
|
+
export function migrateStoredDocument(input: unknown): JsonValue {
|
|
15
|
+
return anchorStrings(dropSectionStatus(migrateDiffSelections(input)));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Lenses saved before ranges were anchor strings. */
|
|
19
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Decoder boundary for stored lenses in retired formats.
|
|
20
|
+
export function migrateStoredLenses(input: unknown): JsonValue {
|
|
21
|
+
return anchorStrings(migrateDiffSelections(input));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ANCHOR_KEYS = ["source", "sources", "callSite", "contextSources"];
|
|
25
|
+
|
|
26
|
+
const selectionObjectSchema = z.object({
|
|
27
|
+
file: z.string(),
|
|
28
|
+
start: z.object({ side: z.enum(["base", "head"]), line: z.number() }),
|
|
29
|
+
end: z.object({ side: z.enum(["base", "head"]), line: z.number() }),
|
|
30
|
+
pins: z.json().optional(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Anchors were once selection objects, each with its own pins. They are now
|
|
35
|
+
* strings, and pins sit on whatever holds them (a code peek, step, frame,
|
|
36
|
+
* attachment or operation) when they differ from the enclosing block's. No
|
|
37
|
+
* stored element mixed pins across its anchors. Lens ranges read at the
|
|
38
|
+
* review's pins and never carried others.
|
|
39
|
+
*/
|
|
40
|
+
export function anchorStrings(
|
|
41
|
+
value: JsonValue,
|
|
42
|
+
blockPins?: JsonValue,
|
|
43
|
+
): JsonValue {
|
|
44
|
+
if (Array.isArray(value))
|
|
45
|
+
return value.map((child) => anchorStrings(child, blockPins));
|
|
46
|
+
|
|
47
|
+
if (!isJsonObject(value)) return value;
|
|
48
|
+
|
|
49
|
+
const inherited = value.pins ?? blockPins;
|
|
50
|
+
let pins: JsonValue | undefined;
|
|
51
|
+
|
|
52
|
+
const convert = (anchor: JsonValue): JsonValue => {
|
|
53
|
+
const selection = selectionObjectSchema.safeParse(anchor);
|
|
54
|
+
|
|
55
|
+
if (!selection.success) return anchor;
|
|
56
|
+
|
|
57
|
+
const { pins: own, ...anchorAt } = selection.data;
|
|
58
|
+
|
|
59
|
+
if (own) pins = own;
|
|
60
|
+
|
|
61
|
+
return formatAnchor(anchorAt);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const migrated = Object.fromEntries(
|
|
65
|
+
Object.entries(value).map(([key, child]) => [
|
|
66
|
+
key,
|
|
67
|
+
ANCHOR_KEYS.includes(key)
|
|
68
|
+
? Array.isArray(child)
|
|
69
|
+
? child.map(convert)
|
|
70
|
+
: convert(child)
|
|
71
|
+
: anchorStrings(child, inherited),
|
|
72
|
+
]),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
pins !== undefined &&
|
|
77
|
+
value.kind !== "ranges" &&
|
|
78
|
+
JSON.stringify(pins) !== JSON.stringify(blockPins)
|
|
79
|
+
)
|
|
80
|
+
migrated.pins = pins;
|
|
81
|
+
|
|
82
|
+
return migrated;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Sections once carried an optional `status` (pending, in_progress or
|
|
86
|
+
* complete). The authoring lease is now the only signal of work in progress,
|
|
87
|
+
* so versions saved before it was retired drop the field when read. */
|
|
88
|
+
function dropSectionStatus(value: JsonValue): JsonValue {
|
|
89
|
+
if (Array.isArray(value)) return value.map(dropSectionStatus);
|
|
90
|
+
|
|
91
|
+
if (!isJsonObject(value)) return value;
|
|
92
|
+
|
|
93
|
+
return Object.fromEntries(
|
|
94
|
+
Object.entries(value)
|
|
95
|
+
.filter(([key]) => !(value.type === "section" && key === "status"))
|
|
96
|
+
.map(([key, child]) => [key, dropSectionStatus(child)]),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A saved `file_lens` block, from before lenses left the document. */
|
|
101
|
+
const legacyFileLensSchema = z.object({
|
|
102
|
+
type: z.literal("file_lens"),
|
|
103
|
+
id: label,
|
|
104
|
+
title: label,
|
|
105
|
+
targets: lensTargetsSchema.optional(),
|
|
106
|
+
patterns: z.array(label).min(1).optional(),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export interface LiftedLenses {
|
|
110
|
+
document: JsonValue;
|
|
111
|
+
lenses: Lens[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Versions saved before lenses moved out of the document hold them as
|
|
115
|
+
* `file_lens` blocks, possibly inside sections. Lift them out of a migrated
|
|
116
|
+
* document, in document order, keeping each id, title and scope (legacy
|
|
117
|
+
* `patterns` become a files target). Viewed state is per file, so it needs
|
|
118
|
+
* no rewrite. */
|
|
119
|
+
export function liftFileLenses(document: JsonValue): LiftedLenses {
|
|
120
|
+
const lenses: Lens[] = [];
|
|
121
|
+
|
|
122
|
+
const visit = (value: JsonValue): JsonValue => {
|
|
123
|
+
if (Array.isArray(value))
|
|
124
|
+
return value.flatMap((item) => {
|
|
125
|
+
if (!isJsonObject(item) || item.type !== "file_lens")
|
|
126
|
+
return [visit(item)];
|
|
127
|
+
|
|
128
|
+
const legacy = legacyFileLensSchema.safeParse(item);
|
|
129
|
+
|
|
130
|
+
// A malformed legacy lens is dropped rather than failing the read.
|
|
131
|
+
if (legacy.success && (legacy.data.targets ?? legacy.data.patterns))
|
|
132
|
+
lenses.push({
|
|
133
|
+
id: legacy.data.id,
|
|
134
|
+
title: legacy.data.title,
|
|
135
|
+
targets: legacy.data.targets ?? [
|
|
136
|
+
{ kind: "files", patterns: legacy.data.patterns! },
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return [];
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!isJsonObject(value)) return value;
|
|
144
|
+
|
|
145
|
+
return Object.fromEntries(
|
|
146
|
+
Object.entries(value).map(([key, child]) => [key, visit(child)]),
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return { document: visit(document), lenses };
|
|
151
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Cleans a free-text telemetry value: replaces paths, web addresses, e-mail
|
|
2
|
+
// addresses, and known secret formats with labelled markers.
|
|
3
|
+
//
|
|
4
|
+
// PROVENANCE. This is a port of Microsoft's implementation in VS Code, which
|
|
5
|
+
// this product is a fork of. Source:
|
|
6
|
+
// apps/review-desktop/code-oss/src/vs/platform/telemetry/common/telemetryUtils.ts
|
|
7
|
+
// (`anonymizeFilePaths`, `userDataRegexes`, `redactIfPossibleUserInfo`,
|
|
8
|
+
// `removePropertiesWithPossibleUserInfo`)
|
|
9
|
+
// Licensed under the MIT License, Copyright (c) Microsoft Corporation.
|
|
10
|
+
//
|
|
11
|
+
// It lives here rather than being imported because the local Review server and
|
|
12
|
+
// the canvas both need it and neither can reach into the vendored VS Code tree.
|
|
13
|
+
// Keep it a FAITHFUL copy: upstream comments and expressions are preserved
|
|
14
|
+
// verbatim so a later reader can diff this file against upstream and see at a
|
|
15
|
+
// glance whether it has drifted. Behaviour changes belong in the caller.
|
|
16
|
+
//
|
|
17
|
+
// The functions are pure and free of Node imports, so the browser-safe event
|
|
18
|
+
// allowlist can share the expression list for its own independent re-check.
|
|
19
|
+
//
|
|
20
|
+
// DEVIATIONS FROM UPSTREAM, and only these:
|
|
21
|
+
// - Upstream keeps the tail of a `.vscode*/extensions/…` path so a stack stays
|
|
22
|
+
// attributable to an extension. Review reports no extension-host errors and
|
|
23
|
+
// cleans a message rather than a stack, so that branch is dropped and such a
|
|
24
|
+
// path is replaced whole. The `node_modules` branch is kept: a message can
|
|
25
|
+
// name a module usefully.
|
|
26
|
+
// - `cleanData`, upstream's object walker, is not ported. Review cleans two
|
|
27
|
+
// known strings, so walking an arbitrary object is not needed, and not
|
|
28
|
+
// having it means there is no path by which an unreviewed property could be
|
|
29
|
+
// cleaned-and-forwarded.
|
|
30
|
+
|
|
31
|
+
export const USER_DATA_REGEXES = [
|
|
32
|
+
{ label: "URL", regex: /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s]*/ },
|
|
33
|
+
{ label: "Google API Key", regex: /AIza[A-Za-z0-9_\\\-]{35}/ },
|
|
34
|
+
{
|
|
35
|
+
label: "JWT",
|
|
36
|
+
regex:
|
|
37
|
+
/eyJ[0eXAiOiJKV1Qi|hbGci|a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+/,
|
|
38
|
+
},
|
|
39
|
+
{ label: "Slack Token", regex: /xox[pbar]\-[A-Za-z0-9]/ },
|
|
40
|
+
{
|
|
41
|
+
label: "GitHub Token",
|
|
42
|
+
regex:
|
|
43
|
+
/(gh[psuro]_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})/,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: "Generic Secret",
|
|
47
|
+
regex:
|
|
48
|
+
/(key|token|sig|secret|signature|password|passwd|pwd|android:value)[^a-zA-Z0-9]/i,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: "CLI Credentials",
|
|
52
|
+
regex:
|
|
53
|
+
/((login|psexec|(certutil|psexec)\.exe).{1,50}(\s-u(ser(name)?)?\s+.{3,100})?\s-(admin|user|vm|root)?p(ass(word)?)?\s+["']?[^$\-\/\s]|(^|[\s\r\n\\])net(\.exe)?.{1,5}(user\s+|share\s+\/user:| user -? secrets ? set) \s + [^ $\s \/])/,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: "Microsoft Entra ID",
|
|
57
|
+
regex: /eyJ(?:0eXAiOiJKV1Qi|hbGci|[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.)/,
|
|
58
|
+
},
|
|
59
|
+
{ label: "Email", regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/ },
|
|
60
|
+
] as const;
|
|
61
|
+
|
|
62
|
+
const REDACTED_PATH_MARKER = "<REDACTED: user-file-path>";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The path shape the cleaner replaces: one or more `segment/` runs, with an
|
|
66
|
+
* optional drive letter, UNC, or `file://` root. Upstream keeps this inline in
|
|
67
|
+
* `anonymizeFilePaths`; it is named here so the allowlist can assert that no
|
|
68
|
+
* path shape survived cleaning.
|
|
69
|
+
*/
|
|
70
|
+
const FILE_PATH_PATTERN =
|
|
71
|
+
/(file:\/\/)?([a-zA-Z]:(\\\\|\\|\/)|(\\\\|\\|\/))?([\w\-\._@]+(\\\\|\\|\/))+[\w\-\._@]*/;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* True when the value still holds something path-shaped. Deliberately the same
|
|
75
|
+
* definition the cleaner uses: the point of re-testing is not a second opinion
|
|
76
|
+
* on what a path looks like, it is to catch the cleaner failing to apply its own
|
|
77
|
+
* rule — wrong patterns, wrong order, or an exception swallowed on the way.
|
|
78
|
+
*/
|
|
79
|
+
export function containsFilePath(value: string): boolean {
|
|
80
|
+
return FILE_PATH_PATTERN.test(value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* True when the value still holds something the cleaner should have replaced.
|
|
85
|
+
* The event allowlist calls this as its own second check, so it must not depend
|
|
86
|
+
* on the cleaner having run.
|
|
87
|
+
*/
|
|
88
|
+
export function hasPossibleUserInfo(value: string): boolean {
|
|
89
|
+
return USER_DATA_REGEXES.some((entry) => entry.regex.test(value));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Cleans a given stack of possible paths
|
|
94
|
+
* @param stack The stack to sanitize
|
|
95
|
+
* @param cleanupPatterns Cleanup patterns to remove from the stack
|
|
96
|
+
* @returns The cleaned stack
|
|
97
|
+
*/
|
|
98
|
+
export function anonymizeFilePaths(
|
|
99
|
+
stack: string,
|
|
100
|
+
cleanupPatterns: RegExp[],
|
|
101
|
+
): string {
|
|
102
|
+
// Fast check to see if it is a file path to avoid doing unnecessary heavy regex work
|
|
103
|
+
if (!stack || (!stack.includes("/") && !stack.includes("\\"))) {
|
|
104
|
+
return stack;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let updatedStack = stack;
|
|
108
|
+
|
|
109
|
+
const cleanUpIndexes: [number, number][] = [];
|
|
110
|
+
|
|
111
|
+
for (const regexp of cleanupPatterns) {
|
|
112
|
+
while (true) {
|
|
113
|
+
const result = regexp.exec(stack);
|
|
114
|
+
|
|
115
|
+
if (!result) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
cleanUpIndexes.push([result.index, regexp.lastIndex]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Match node_modules or node_modules.asar at any position in the path, capturing the node_modules/... suffix
|
|
124
|
+
const nodeModulesRegex =
|
|
125
|
+
/(?:^|[\\\/])((node_modules|node_modules\.asar)[\\\/].*)$/;
|
|
126
|
+
|
|
127
|
+
const fileRegex =
|
|
128
|
+
/(file:\/\/)?([a-zA-Z]:(\\\\|\\|\/)|(\\\\|\\|\/))?([\w\-\._@]+(\\\\|\\|\/))+[\w\-\._@]*/g;
|
|
129
|
+
|
|
130
|
+
let lastIndex = 0;
|
|
131
|
+
updatedStack = "";
|
|
132
|
+
|
|
133
|
+
while (true) {
|
|
134
|
+
const result = fileRegex.exec(stack);
|
|
135
|
+
|
|
136
|
+
if (!result) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check to see if the any cleanupIndexes partially overlap with this match
|
|
141
|
+
const overlappingRange = cleanUpIndexes.some(
|
|
142
|
+
([start, end]) => result.index < end && start < fileRegex.lastIndex,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// anoynimize user file paths that do not need to be retained or cleaned up.
|
|
146
|
+
if (!overlappingRange) {
|
|
147
|
+
// Check if node_modules appears in the path — preserve node_modules/... suffix
|
|
148
|
+
const nodeModulesMatch = nodeModulesRegex.exec(result[0]);
|
|
149
|
+
|
|
150
|
+
if (nodeModulesMatch) {
|
|
151
|
+
updatedStack +=
|
|
152
|
+
stack.substring(lastIndex, result.index) +
|
|
153
|
+
`${REDACTED_PATH_MARKER}/` +
|
|
154
|
+
nodeModulesMatch[1];
|
|
155
|
+
} else {
|
|
156
|
+
updatedStack +=
|
|
157
|
+
stack.substring(lastIndex, result.index) + REDACTED_PATH_MARKER;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
lastIndex = fileRegex.lastIndex;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (lastIndex < stack.length) {
|
|
165
|
+
updatedStack += stack.substring(lastIndex);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return updatedStack;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Redacts a value if it contains commonly leaked PII.
|
|
173
|
+
* @param value The value returned (as-is) when no PII is detected
|
|
174
|
+
* @param probe The string actually matched against the PII heuristics. Defaults
|
|
175
|
+
* to `value`; callers may pass a value that includes a trailing delimiter (e.g. a
|
|
176
|
+
* newline) so that heuristics relying on a non-alphanumeric boundary match the
|
|
177
|
+
* same way they would against the original whole string.
|
|
178
|
+
* @returns A `<REDACTED: ...>` marker if the probe matched, otherwise `value`
|
|
179
|
+
*/
|
|
180
|
+
function redactIfPossibleUserInfo(
|
|
181
|
+
value: string,
|
|
182
|
+
probe: string = value,
|
|
183
|
+
): string {
|
|
184
|
+
for (const secretRegex of USER_DATA_REGEXES) {
|
|
185
|
+
if (secretRegex.regex.test(probe)) {
|
|
186
|
+
return `<REDACTED: ${secretRegex.label}>`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Attempts to remove commonly leaked PII.
|
|
195
|
+
*
|
|
196
|
+
* When a match is found the check is applied per line so that a single suspicious
|
|
197
|
+
* frame (e.g. a stack frame containing a function name such as `getStorageKey`
|
|
198
|
+
* which matches the broad `Generic Secret` heuristic) only redacts that line —
|
|
199
|
+
* replacing it with a `<REDACTED: ...>` marker — instead of wiping the entire
|
|
200
|
+
* multi-line value such as a whole callstack.
|
|
201
|
+
* @param property The property whose offending lines will be replaced with a redaction marker if they contain user data
|
|
202
|
+
* @returns The new value for the property
|
|
203
|
+
*/
|
|
204
|
+
export function removePropertiesWithPossibleUserInfo(property: string): string {
|
|
205
|
+
// If for some reason it is undefined we skip it (this shouldn't be possible);
|
|
206
|
+
if (!property) {
|
|
207
|
+
return property;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Fast path: if nothing matches we return the value untouched without
|
|
211
|
+
// allocating.
|
|
212
|
+
if (!hasPossibleUserInfo(property)) {
|
|
213
|
+
return property;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Single line values keep the original behavior of redacting the whole value.
|
|
217
|
+
if (!property.includes("\n")) {
|
|
218
|
+
return redactIfPossibleUserInfo(property);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Multi-line values (e.g. callstacks) are redacted line-by-line so we only
|
|
222
|
+
// drop the offending lines and preserve the rest of the information.
|
|
223
|
+
const lines = property.split("\n");
|
|
224
|
+
|
|
225
|
+
for (let i = 0; i < lines.length; i++) {
|
|
226
|
+
const probe = i < lines.length - 1 ? lines[i] + "\n" : lines[i];
|
|
227
|
+
lines[i] = redactIfPossibleUserInfo(lines[i], probe);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return lines.join("\n");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The whole pipeline for one free-text value, in VS Code's order: replace
|
|
235
|
+
* path-shaped runs, delete the known local directories outright, then replace
|
|
236
|
+
* any line holding a secret or address shape.
|
|
237
|
+
*/
|
|
238
|
+
export function cleanTelemetryText(
|
|
239
|
+
value: string,
|
|
240
|
+
cleanupPatterns: RegExp[],
|
|
241
|
+
): string {
|
|
242
|
+
let updated = value.replaceAll("%20", " ");
|
|
243
|
+
updated = anonymizeFilePaths(updated, cleanupPatterns);
|
|
244
|
+
|
|
245
|
+
for (const regexp of cleanupPatterns) {
|
|
246
|
+
updated = updated.replace(regexp, "");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return removePropertiesWithPossibleUserInfo(updated);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// `cleanupPatterns` above is the list of directories to delete outright rather
|
|
253
|
+
// than replace with a marker. Upstream builds it from five known directories;
|
|
254
|
+
// Review always passes an empty list, so what remains is the overlap check
|
|
255
|
+
// inherited from upstream. See NO_DELETED_DIRECTORIES in error-telemetry.ts for
|
|
256
|
+
// why. Should Review ever need the list, upstream builds it inline in
|
|
257
|
+
// telemetryService.ts with escaped, case-insensitive, global patterns.
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { JsonValue } from "@dev.fast/review-protocol";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import { findReviewPackageRoot } from "./package-paths";
|
|
8
|
+
import { DEV_REVIEW_HOME_ENV, devReviewHome } from "./review-home-paths";
|
|
9
|
+
|
|
10
|
+
export interface ReviewTelemetryInstallConfig {
|
|
11
|
+
installationId: string;
|
|
12
|
+
/**
|
|
13
|
+
* When this install was created, ISO 8601. A config written before the
|
|
14
|
+
* field existed gets the first time a later version read it.
|
|
15
|
+
*/
|
|
16
|
+
createdAt: string;
|
|
17
|
+
installationCreatedSent: boolean;
|
|
18
|
+
firstReviewPresentedSent: boolean;
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
internal: boolean;
|
|
21
|
+
/** `gh_` + keyed hash of the signed-in account id; absent until a login. */
|
|
22
|
+
accountAlias?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ReviewTelemetryChannel = "stable" | "preview" | "dev";
|
|
26
|
+
|
|
27
|
+
export type ReviewTelemetryEnvironment =
|
|
28
|
+
| "production"
|
|
29
|
+
| "ci"
|
|
30
|
+
| "internal"
|
|
31
|
+
| "e2e"
|
|
32
|
+
| "smoke";
|
|
33
|
+
|
|
34
|
+
export type ReviewTelemetrySurface =
|
|
35
|
+
| "desktop"
|
|
36
|
+
| "cli"
|
|
37
|
+
| "headless"
|
|
38
|
+
| "mcp"
|
|
39
|
+
| "api";
|
|
40
|
+
|
|
41
|
+
/** Set by Electron main from product.json `quality`; `dev` for an unpackaged run. */
|
|
42
|
+
export const REVIEW_CHANNEL_ENV = "DEV_FAST_REVIEW_CHANNEL";
|
|
43
|
+
|
|
44
|
+
/** Set by the e2e harness (`e2e`) and the packaged smoke scripts (`smoke`). */
|
|
45
|
+
export const REVIEW_TELEMETRY_ENV_ENV = "DEV_FAST_REVIEW_TELEMETRY_ENV";
|
|
46
|
+
|
|
47
|
+
const CHANNELS: readonly ReviewTelemetryChannel[] = [
|
|
48
|
+
"stable",
|
|
49
|
+
"preview",
|
|
50
|
+
"dev",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* PostHog groups events into sessions by `$session_id` and accepts only a
|
|
55
|
+
* UUIDv7 there, so only a v7 app session id doubles as one.
|
|
56
|
+
*/
|
|
57
|
+
export const uuidV7Schema = z
|
|
58
|
+
.string()
|
|
59
|
+
.regex(
|
|
60
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
export function isUuidV7(value: unknown): value is string {
|
|
64
|
+
return uuidV7Schema.safeParse(value).success;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function reviewTelemetryChannel(
|
|
68
|
+
env: NodeJS.ProcessEnv,
|
|
69
|
+
): ReviewTelemetryChannel {
|
|
70
|
+
// SAFETY: the cast is provisional; CHANNELS.includes(value) below checks
|
|
71
|
+
// membership before the value is ever returned, falling back to "stable".
|
|
72
|
+
const value = env[REVIEW_CHANNEL_ENV]?.trim() as ReviewTelemetryChannel;
|
|
73
|
+
|
|
74
|
+
return CHANNELS.includes(value) ? value : "stable";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* First match wins: a harness declares itself, then CI, then a dev.fast
|
|
79
|
+
* checkout or a persisted internal marker, else a real user.
|
|
80
|
+
*/
|
|
81
|
+
export function reviewTelemetryEnvironment(
|
|
82
|
+
env: NodeJS.ProcessEnv,
|
|
83
|
+
config?: Pick<ReviewTelemetryInstallConfig, "internal">,
|
|
84
|
+
): ReviewTelemetryEnvironment {
|
|
85
|
+
const harness = env[REVIEW_TELEMETRY_ENV_ENV]?.trim();
|
|
86
|
+
|
|
87
|
+
if (harness === "e2e" || harness === "smoke") return harness;
|
|
88
|
+
|
|
89
|
+
if (env.CI) return "ci";
|
|
90
|
+
|
|
91
|
+
if (isInternalTelemetry(env, config)) return "internal";
|
|
92
|
+
|
|
93
|
+
return "production";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const TELEMETRY_CONFIG_RELATIVE_PATH = path.join(
|
|
97
|
+
"telemetry",
|
|
98
|
+
"progressive-review.json",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const PREVIEW_TELEMETRY_CONFIG_RELATIVE_PATH = path.join(
|
|
102
|
+
"telemetry",
|
|
103
|
+
"progressive-review.preview.json",
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const LEGACY_APP_TELEMETRY_CONFIG_RELATIVE_PATH = path.join(
|
|
107
|
+
"telemetry",
|
|
108
|
+
"install.json",
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
export function reviewTelemetryConfigPath(
|
|
112
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
113
|
+
): string {
|
|
114
|
+
return path.join(
|
|
115
|
+
devReviewHome(env),
|
|
116
|
+
reviewTelemetryChannel(env) === "preview"
|
|
117
|
+
? PREVIEW_TELEMETRY_CONFIG_RELATIVE_PATH
|
|
118
|
+
: TELEMETRY_CONFIG_RELATIVE_PATH,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function legacyAppTelemetryConfigPath(
|
|
123
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
124
|
+
): string {
|
|
125
|
+
return path.join(
|
|
126
|
+
devReviewHome(env),
|
|
127
|
+
LEGACY_APP_TELEMETRY_CONFIG_RELATIVE_PATH,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function isTelemetryOptedOut(
|
|
132
|
+
env: NodeJS.ProcessEnv,
|
|
133
|
+
config?: Pick<ReviewTelemetryInstallConfig, "enabled">,
|
|
134
|
+
): boolean {
|
|
135
|
+
// Test runners must never emit real telemetry: every vitest/node-test run
|
|
136
|
+
// with a temp DEV_REVIEW_HOME mints a fresh installation id and floods the
|
|
137
|
+
// installation and command metrics. Telemetry's own unit tests inject fake
|
|
138
|
+
// capture clients, so they are unaffected by this guard.
|
|
139
|
+
if (isEnabledEnvValue(env.VITEST) || env.NODE_ENV === "test") return true;
|
|
140
|
+
|
|
141
|
+
if (config?.enabled === false) return true;
|
|
142
|
+
|
|
143
|
+
// Keep every historical spelling so existing shell and CI configurations
|
|
144
|
+
// continue to disable telemetry after package and product renames.
|
|
145
|
+
return [
|
|
146
|
+
env.DO_NOT_TRACK,
|
|
147
|
+
env.DNT,
|
|
148
|
+
env.PROGRESSIVE_REVIEW_TELEMETRY_DISABLED,
|
|
149
|
+
env.DEV_FAST_TELEMETRY_DISABLED,
|
|
150
|
+
env.DEV_FAST_PROGRESSIVE_REVIEW_TELEMETRY_DISABLED,
|
|
151
|
+
env.DEV_FAST_REVIEW_TELEMETRY_DISABLED,
|
|
152
|
+
].some(isEnabledEnvValue);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The on-disk install config as a hand-edited or older file may hold it: only
|
|
157
|
+
* the installation id is required, and a malformed optional field reads as
|
|
158
|
+
* absent.
|
|
159
|
+
*/
|
|
160
|
+
const storedTelemetryInstallConfigSchema = z.looseObject({
|
|
161
|
+
installationId: z.string().min(1),
|
|
162
|
+
createdAt: z.iso.datetime({ offset: true }).optional().catch(undefined),
|
|
163
|
+
installationCreatedSent: z.boolean().optional().catch(undefined),
|
|
164
|
+
firstReviewPresentedSent: z.boolean().optional().catch(undefined),
|
|
165
|
+
enabled: z.boolean().optional().catch(undefined),
|
|
166
|
+
internal: z.boolean().optional().catch(undefined),
|
|
167
|
+
accountAlias: z.string().min(1).optional().catch(undefined),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
export function normalizeTelemetryInstallConfig(
|
|
171
|
+
parsed: JsonValue,
|
|
172
|
+
now: () => Date,
|
|
173
|
+
): ReviewTelemetryInstallConfig | undefined {
|
|
174
|
+
const stored = storedTelemetryInstallConfigSchema.safeParse(parsed);
|
|
175
|
+
|
|
176
|
+
if (!stored.success) return undefined;
|
|
177
|
+
|
|
178
|
+
const config: ReviewTelemetryInstallConfig = {
|
|
179
|
+
installationId: stored.data.installationId,
|
|
180
|
+
createdAt: stored.data.createdAt ?? now().toISOString(),
|
|
181
|
+
installationCreatedSent: stored.data.installationCreatedSent === true,
|
|
182
|
+
firstReviewPresentedSent: stored.data.firstReviewPresentedSent === true,
|
|
183
|
+
enabled: stored.data.enabled !== false,
|
|
184
|
+
internal: stored.data.internal === true,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (stored.data.accountAlias) config.accountAlias = stored.data.accountAlias;
|
|
188
|
+
|
|
189
|
+
return config;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Whether normalizing changed a field an older or hand-edited file lacked, so
|
|
194
|
+
* the config must be written back once: a backfilled `createdAt` has to stay
|
|
195
|
+
* the first-seen time rather than move with every read.
|
|
196
|
+
*/
|
|
197
|
+
export function telemetryInstallConfigNeedsWrite(
|
|
198
|
+
parsed: JsonValue,
|
|
199
|
+
config: ReviewTelemetryInstallConfig,
|
|
200
|
+
): boolean {
|
|
201
|
+
const stored = storedTelemetryInstallConfigSchema.safeParse(parsed).data;
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
stored?.internal !== config.internal ||
|
|
205
|
+
stored?.createdAt !== config.createdAt
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
210
|
+
|
|
211
|
+
/** Whole days since the install was created; 0 for a clock set back. */
|
|
212
|
+
export function installAgeDays(
|
|
213
|
+
config: Pick<ReviewTelemetryInstallConfig, "createdAt">,
|
|
214
|
+
now: Date,
|
|
215
|
+
): number {
|
|
216
|
+
const createdAt = Date.parse(config.createdAt);
|
|
217
|
+
|
|
218
|
+
if (Number.isNaN(createdAt)) return 0;
|
|
219
|
+
|
|
220
|
+
return Math.max(0, Math.floor((now.getTime() - createdAt) / DAY_MS));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function createTelemetryInstallConfig(
|
|
224
|
+
installationId: string,
|
|
225
|
+
now: () => Date,
|
|
226
|
+
): ReviewTelemetryInstallConfig {
|
|
227
|
+
return {
|
|
228
|
+
installationId,
|
|
229
|
+
createdAt: now().toISOString(),
|
|
230
|
+
installationCreatedSent: false,
|
|
231
|
+
firstReviewPresentedSent: false,
|
|
232
|
+
enabled: true,
|
|
233
|
+
internal: false,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isEnabledEnvValue(value: string | undefined): boolean {
|
|
238
|
+
return value === "1" || value?.toLowerCase() === "true";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let cachedWorkspaceCheckout: boolean | undefined;
|
|
242
|
+
|
|
243
|
+
// Running from a workspace checkout (the dev.fast monorepo or any pnpm
|
|
244
|
+
// workspace clone) means the traffic is ours, not a customer's. A published
|
|
245
|
+
// npm install always lives under node_modules and has no workspace manifest
|
|
246
|
+
// above it.
|
|
247
|
+
function isWorkspaceCheckout(): boolean {
|
|
248
|
+
if (cachedWorkspaceCheckout !== undefined) return cachedWorkspaceCheckout;
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const packageRoot = findReviewPackageRoot();
|
|
252
|
+
|
|
253
|
+
if (packageRoot.split(path.sep).includes("node_modules")) {
|
|
254
|
+
cachedWorkspaceCheckout = false;
|
|
255
|
+
|
|
256
|
+
return cachedWorkspaceCheckout;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let dir = packageRoot;
|
|
260
|
+
|
|
261
|
+
while (true) {
|
|
262
|
+
if (existsSync(path.join(dir, "pnpm-workspace.yaml"))) {
|
|
263
|
+
cachedWorkspaceCheckout = true;
|
|
264
|
+
|
|
265
|
+
return cachedWorkspaceCheckout;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const parent = path.dirname(dir);
|
|
269
|
+
|
|
270
|
+
if (parent === dir) break;
|
|
271
|
+
dir = parent;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
cachedWorkspaceCheckout = false;
|
|
275
|
+
} catch {
|
|
276
|
+
cachedWorkspaceCheckout = false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return cachedWorkspaceCheckout;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Whether telemetry from this process should carry `internal: true` so
|
|
284
|
+
* partner-facing dashboards can exclude it. The environment overrides a
|
|
285
|
+
* stored true marker, which overrides workspace detection.
|
|
286
|
+
*/
|
|
287
|
+
export function isInternalTelemetry(
|
|
288
|
+
env: NodeJS.ProcessEnv,
|
|
289
|
+
config?: Pick<ReviewTelemetryInstallConfig, "internal">,
|
|
290
|
+
): boolean {
|
|
291
|
+
if (env.PROGRESSIVE_REVIEW_TELEMETRY_INTERNAL === "1") return true;
|
|
292
|
+
|
|
293
|
+
if (env.PROGRESSIVE_REVIEW_TELEMETRY_INTERNAL === "0") return false;
|
|
294
|
+
|
|
295
|
+
if (config?.internal === true) return true;
|
|
296
|
+
|
|
297
|
+
return isWorkspaceCheckout();
|
|
298
|
+
}
|