@dev.fast/whiteboard 0.0.0-stage → 0.2.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.
Files changed (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -2
  3. package/THIRD_PARTY_NOTICES.md +93 -0
  4. package/dist/account-alias-DOHN1RSH.js +973 -0
  5. package/dist/agent-cli-ChOq0ZuO.js +117 -0
  6. package/dist/agent-cli-DzWfZr6G.js +2 -0
  7. package/dist/agent-client-CPQo7iTI.js +305 -0
  8. package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
  9. package/dist/build-info.json +1 -0
  10. package/dist/cli-hVgHcdsv.js +47 -0
  11. package/dist/cli-runner-D8y2_luU.js +5095 -0
  12. package/dist/cli.d.ts +1 -0
  13. package/dist/cli.js +128 -0
  14. package/dist/client-DFoh50W3.js +950 -0
  15. package/dist/desktop-discovery-DBO-PM2V.js +164 -0
  16. package/dist/error-message-OtiDonty.js +6 -0
  17. package/dist/fs-utils-BMPLt0cr.js +22 -0
  18. package/dist/fuzzy-match-BoAmcyak.js +30 -0
  19. package/dist/headless-host-BJxf4dUU.js +94 -0
  20. package/dist/input-error-OLgB_h31.js +11 -0
  21. package/dist/local-data-CxLk75rx.js +4491 -0
  22. package/dist/mcp-QeF8vkdf.js +140 -0
  23. package/dist/package-paths-B6-zxvIO.js +47 -0
  24. package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
  25. package/dist/profile-4ry2f3AM.js +76 -0
  26. package/dist/profile-Bh5FIrmf.js +2 -0
  27. package/dist/request-origin-D_4QIdoN.js +811 -0
  28. package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
  29. package/dist/review-home-paths-6zZH-1c9.js +2560 -0
  30. package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
  31. package/dist/runtime-CKiWhGrf.js +52 -0
  32. package/dist/runtime.d.ts +9 -0
  33. package/dist/runtime.js +2 -0
  34. package/dist/s3-SzLpeCG6.js +345 -0
  35. package/dist/s3-config-BgxkSoOy.js +2 -0
  36. package/dist/s3-config-CAxhO9u_.js +676 -0
  37. package/dist/server/desktop-host.d.ts +4 -0
  38. package/dist/server/desktop-host.js +1702 -0
  39. package/dist/server-discovery-Dbzn3w6Z.js +66 -0
  40. package/dist/sharing/index.d.ts +1977 -0
  41. package/dist/sharing/index.js +2 -0
  42. package/dist/src-CGP5ytbV.js +283 -0
  43. package/dist/src-CmdiBL20.js +3676 -0
  44. package/dist/src-DnwdaQ2r.js +865 -0
  45. package/dist/src-X9phtB2j.js +68 -0
  46. package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
  47. package/dist/tool-failure-C8zB73HV.js +98 -0
  48. package/dist/tutorial-trace-DrBvkaeL.js +43 -0
  49. package/instructions/authoring.md +36 -0
  50. package/instructions/file-lenses.md +13 -0
  51. package/instructions/scratchpad.md +23 -0
  52. package/instructions/trace-archaeology.md +113 -0
  53. package/onboarding.md +8 -0
  54. package/package.json +106 -3
  55. package/src/agent-selection.ts +100 -0
  56. package/src/agent-session-ref.ts +80 -0
  57. package/src/ask/agents.ts +351 -0
  58. package/src/ask/checkout-files.ts +47 -0
  59. package/src/ask/file-refs.ts +111 -0
  60. package/src/ask/pi-mcp.ts +154 -0
  61. package/src/ask/protocol.ts +244 -0
  62. package/src/ask/thread-state.ts +350 -0
  63. package/src/ask/thread.ts +1400 -0
  64. package/src/ask/threads.ts +219 -0
  65. package/src/ask/watch.ts +79 -0
  66. package/src/cli-install.ts +1041 -0
  67. package/src/cli-runner.ts +1461 -0
  68. package/src/cli-runtime-info.ts +35 -0
  69. package/src/cli.ts +222 -0
  70. package/src/connect-prompts.ts +248 -0
  71. package/src/cursor-deeplink.ts +11 -0
  72. package/src/desktop-discovery.ts +355 -0
  73. package/src/diff-selection-migration.ts +63 -0
  74. package/src/embedded-posthog-key.ts +6 -0
  75. package/src/error-telemetry.ts +247 -0
  76. package/src/evidence.ts +14 -0
  77. package/src/exception-telemetry.ts +126 -0
  78. package/src/fixtures/blocks/call_stack_diff.json +27 -0
  79. package/src/fixtures/blocks/callout.json +11 -0
  80. package/src/fixtures/blocks/code.json +9 -0
  81. package/src/fixtures/blocks/code_peek.json +8 -0
  82. package/src/fixtures/blocks/database_lens.json +51 -0
  83. package/src/fixtures/blocks/divider.json +1 -0
  84. package/src/fixtures/blocks/fixtures.ts +33 -0
  85. package/src/fixtures/blocks/flow_diagram.json +36 -0
  86. package/src/fixtures/blocks/ids.ts +8 -0
  87. package/src/fixtures/blocks/image.json +9 -0
  88. package/src/fixtures/blocks/markdown.json +7 -0
  89. package/src/fixtures/blocks/section.json +11 -0
  90. package/src/fixtures/blocks/sequence.json +31 -0
  91. package/src/fixtures/blocks/software_map.json +7 -0
  92. package/src/fixtures/blocks/trace_quote.json +9 -0
  93. package/src/fixtures/blocks/tutorial.json +51 -0
  94. package/src/fs-utils.ts +32 -0
  95. package/src/fuzzy-match.ts +94 -0
  96. package/src/install.ts +29 -0
  97. package/src/legacy-skills.ts +133 -0
  98. package/src/lens-selection.ts +230 -0
  99. package/src/markdown-latex-math.ts +230 -0
  100. package/src/markdown.ts +81 -0
  101. package/src/package-paths.ts +53 -0
  102. package/src/posthog-capture-client.ts +610 -0
  103. package/src/review-api/README.md +295 -0
  104. package/src/review-api/activity.ts +340 -0
  105. package/src/review-api/agent-cli.ts +219 -0
  106. package/src/review-api/agent-client.ts +188 -0
  107. package/src/review-api/anchor-quotes.ts +88 -0
  108. package/src/review-api/ask-history.ts +242 -0
  109. package/src/review-api/authoring-tools.ts +211 -0
  110. package/src/review-api/blocks/call_stack_diff.ts +75 -0
  111. package/src/review-api/blocks/callout.ts +24 -0
  112. package/src/review-api/blocks/code.ts +9 -0
  113. package/src/review-api/blocks/code_peek.ts +13 -0
  114. package/src/review-api/blocks/database_lens.ts +150 -0
  115. package/src/review-api/blocks/definition.ts +40 -0
  116. package/src/review-api/blocks/divider.ts +6 -0
  117. package/src/review-api/blocks/flow_diagram.ts +111 -0
  118. package/src/review-api/blocks/image.ts +10 -0
  119. package/src/review-api/blocks/index.ts +90 -0
  120. package/src/review-api/blocks/markdown.ts +17 -0
  121. package/src/review-api/blocks/section.ts +24 -0
  122. package/src/review-api/blocks/sequence.ts +56 -0
  123. package/src/review-api/blocks/software_map.ts +9 -0
  124. package/src/review-api/blocks/trace_quote.ts +10 -0
  125. package/src/review-api/blocks/tutorial.ts +39 -0
  126. package/src/review-api/checkout-fs.ts +14 -0
  127. package/src/review-api/client.ts +1 -0
  128. package/src/review-api/comparison-coverage.ts +304 -0
  129. package/src/review-api/component-reference.ts +30 -0
  130. package/src/review-api/diff-lenses.ts +175 -0
  131. package/src/review-api/document-headings.ts +51 -0
  132. package/src/review-api/document-text.ts +200 -0
  133. package/src/review-api/document.ts +906 -0
  134. package/src/review-api/file-lenses.ts +100 -0
  135. package/src/review-api/http.ts +1877 -0
  136. package/src/review-api/image-decode.ts +29 -0
  137. package/src/review-api/input-error.ts +9 -0
  138. package/src/review-api/instructions.ts +89 -0
  139. package/src/review-api/lens-alignment.ts +52 -0
  140. package/src/review-api/local-data.ts +1837 -0
  141. package/src/review-api/map-input.ts +154 -0
  142. package/src/review-api/mcp-client-agent.ts +32 -0
  143. package/src/review-api/mcp.ts +240 -0
  144. package/src/review-api/origin.ts +43 -0
  145. package/src/review-api/profile.ts +146 -0
  146. package/src/review-api/public-tools.ts +102 -0
  147. package/src/review-api/pull-request.ts +389 -0
  148. package/src/review-api/read-schemas.ts +85 -0
  149. package/src/review-api/recovery.ts +2 -0
  150. package/src/review-api/request-origin.ts +33 -0
  151. package/src/review-api/review-progress.ts +382 -0
  152. package/src/review-api/status-tool.ts +10 -0
  153. package/src/review-api/store-schema.ts +22 -0
  154. package/src/review-api/store.ts +1647 -0
  155. package/src/review-api/tool-failure.ts +64 -0
  156. package/src/review-api/trace-schema.ts +13 -0
  157. package/src/review-api/traces.ts +119 -0
  158. package/src/review-api/unsupported-files.integration.ts +91 -0
  159. package/src/review-api/workspaces.ts +692 -0
  160. package/src/review-api/worktree-source.ts +170 -0
  161. package/src/review-api/worktree-structural.integration.ts +320 -0
  162. package/src/review-app-launcher.ts +432 -0
  163. package/src/review-app-picker.ts +168 -0
  164. package/src/review-app.ts +134 -0
  165. package/src/review-bundled-tools.ts +249 -0
  166. package/src/review-checkout-paths.ts +37 -0
  167. package/src/review-diff-files.ts +89 -0
  168. package/src/review-head-checkout.ts +300 -0
  169. package/src/review-home-paths.ts +73 -0
  170. package/src/review-info.ts +59 -0
  171. package/src/review-instances.ts +113 -0
  172. package/src/review-logger.ts +183 -0
  173. package/src/review-preferences.ts +88 -0
  174. package/src/review-prepare.ts +291 -0
  175. package/src/review-stack.ts +112 -0
  176. package/src/review-telemetry.ts +1066 -0
  177. package/src/runtime.ts +74 -0
  178. package/src/server/account-alias.ts +30 -0
  179. package/src/server/bounded-stream.ts +34 -0
  180. package/src/server/bug-report.ts +301 -0
  181. package/src/server/client-error-budget.ts +45 -0
  182. package/src/server/crash-report.ts +273 -0
  183. package/src/server/desktop-host-shutdown.ts +64 -0
  184. package/src/server/desktop-host.ts +190 -0
  185. package/src/server/desktop-server.ts +703 -0
  186. package/src/server/diffr-config.ts +509 -0
  187. package/src/server/diffr-languages.ts +97 -0
  188. package/src/server/global-verb-relay.ts +190 -0
  189. package/src/server/headless-host.ts +149 -0
  190. package/src/server/hono-http.ts +162 -0
  191. package/src/server/http-json.ts +24 -0
  192. package/src/server/json-review-reporting.ts +174 -0
  193. package/src/server/process-error-telemetry.ts +174 -0
  194. package/src/server/review-api-parsers.ts +90 -0
  195. package/src/server/review-lifecycle-telemetry.ts +73 -0
  196. package/src/server/review-open-watchdog.ts +46 -0
  197. package/src/server/review-server-core.ts +290 -0
  198. package/src/server/structural-comparisons.ts +153 -0
  199. package/src/server/structural-diff.ts +186 -0
  200. package/src/server/tutorial-service.ts +241 -0
  201. package/src/server/ui-telemetry.ts +198 -0
  202. package/src/server-discovery.ts +95 -0
  203. package/src/session-markers.ts +132 -0
  204. package/src/sharing/auth.ts +41 -0
  205. package/src/sharing/cli.ts +83 -0
  206. package/src/sharing/client.ts +334 -0
  207. package/src/sharing/export.ts +195 -0
  208. package/src/sharing/host.ts +316 -0
  209. package/src/sharing/import.ts +787 -0
  210. package/src/sharing/index.ts +16 -0
  211. package/src/sharing/repository.ts +145 -0
  212. package/src/sharing/routes.ts +34 -0
  213. package/src/slug.ts +23 -0
  214. package/src/software-map-diff-counts.ts +517 -0
  215. package/src/software-map-model.ts +1147 -0
  216. package/src/software-map-topology-diff.ts +260 -0
  217. package/src/source.ts +92 -0
  218. package/src/startup-trace.ts +232 -0
  219. package/src/stored-document-migration.ts +151 -0
  220. package/src/telemetry-clean-text.ts +257 -0
  221. package/src/telemetry-config.ts +298 -0
  222. package/src/telemetry-debug-sink.ts +38 -0
  223. package/src/telemetry.ts +13 -0
  224. package/src/trace-cli.ts +156 -0
  225. package/src/trace-storage-cli.ts +509 -0
  226. package/src/tutorial-conversation.ts +18 -0
  227. package/src/ui-telemetry-events.ts +765 -0
  228. package/src/unified-diff.ts +71 -0
  229. package/src/viewed-coverage.ts +259 -0
  230. package/src/windows-cli.ts +125 -0
  231. package/tutorial/document.json +275 -0
  232. package/tutorial/runtime-manifest.json +12 -0
  233. package/tutorial/sample-service/package.json +9 -0
  234. package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
  235. package/tutorial/sample-service/src/app.ts +26 -0
  236. package/tutorial/sample-service/src/database/schema.ts +10 -0
  237. package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
  238. package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
  239. package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
  240. package/tutorial/sample-service/src/orders/order-service.ts +34 -0
  241. package/tutorial/sample-service/src/orders/order.ts +21 -0
  242. package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
  243. package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
  244. package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
  245. package/tutorial/sample-service/tsconfig.json +12 -0
  246. package/tutorial/software-map.json +139 -0
  247. package/tutorial/trace.json +20 -0
@@ -0,0 +1,765 @@
1
+ // Contract for app-originated telemetry events.
2
+ //
3
+ // Privacy invariant: telemetry must never carry user content — no review
4
+ // prose, code, symbol names, file paths, locators, quotes, or node labels. The
5
+ // server validates every incoming UI event against this allowlist and DROPS
6
+ // anything not listed here: unknown events, unknown property keys, values
7
+ // outside an enum, non-finite numbers, or oversized values. Adding a property
8
+ // therefore requires extending this table, which is the intended review point
9
+ // for privacy.
10
+ //
11
+ // Error reports are the one place free text is allowed, and it is bounded on
12
+ // both sides. A raw error reaches the local server in the request's `error`
13
+ // envelope, never in `properties`. The server cleans the message with a port of
14
+ // VS Code's cleaner, which replaces paths, addresses and secrets with markers,
15
+ // and this file re-checks the result with `isReportableCleanedMessage` before
16
+ // accepting the property. Either the cleaner finished the job or only the
17
+ // digest goes; a wrong producer cannot put raw text on the wire through here.
18
+
19
+ import {
20
+ type JsonObject,
21
+ REVIEW_DIFF_LAYOUTS,
22
+ REVIEW_THEME_CHOICES,
23
+ isJsonObject,
24
+ jsonBoolean,
25
+ jsonNumber,
26
+ jsonString,
27
+ } from "@dev.fast/review-protocol";
28
+ import { z } from "zod";
29
+
30
+ import { containsFilePath, hasPossibleUserInfo } from "./telemetry-clean-text";
31
+
32
+ /** Property value validators: an enum of allowed strings, or a scalar type. */
33
+ export type UiTelemetryPropertySpec =
34
+ | readonly string[]
35
+ | "number"
36
+ | "boolean"
37
+ | "opaque_id"
38
+ // A bounded semantic release version, never a commit or authored value.
39
+ | "release_version"
40
+ // A short identifier-like string (e.g. a JS error class name). The only
41
+ // free-form value allowed anywhere, still length- and charset-capped.
42
+ | "enum_free_short"
43
+ // A truncated SHA-256 digest. Groups identical error messages without ever
44
+ // carrying the message itself.
45
+ | "hash_hex"
46
+ // Stack frames that resolve inside the shipped bundle, separated by "|".
47
+ // Every frame is re-checked here against BUNDLE_FRAME_PATTERN, so a bug in
48
+ // the producer cannot smuggle a user path through.
49
+ | "bundle_frames"
50
+ // An error message that has been through the cleaner. Re-checked here against
51
+ // the same secret shapes, plus a refusal of any surviving path separator.
52
+ | "cleaned_message";
53
+
54
+ export interface UiTelemetryEventSpec {
55
+ /** PostHog event name (already namespaced). */
56
+ readonly event: string;
57
+ readonly properties: Readonly<Record<string, UiTelemetryPropertySpec>>;
58
+ }
59
+
60
+ const PEEK_VIA = [
61
+ "prose_link",
62
+ "diagram",
63
+ "map",
64
+ "db_lens",
65
+ "call_stack_frame",
66
+ ] as const;
67
+
68
+ export const PEEK_ROOT_KIND = ["range"] as const;
69
+
70
+ export const LSP_FEATURE = [
71
+ "hover",
72
+ "goto_definition",
73
+ "peek_definition",
74
+ "goto_type_definition",
75
+ "goto_implementation",
76
+ "references",
77
+ "rename",
78
+ "format",
79
+ "code_action",
80
+ "symbol_search",
81
+ ] as const;
82
+
83
+ export const LSP_VIA = ["command", "mouse"] as const;
84
+
85
+ export const LSP_EDITOR_KIND = ["files_tab", "inline_peek", "diff"] as const;
86
+
87
+ export const LSP_LANGUAGE = [
88
+ "typescript",
89
+ "javascript",
90
+ "python",
91
+ "go",
92
+ "rust",
93
+ "swift",
94
+ "csharp",
95
+ "json",
96
+ "css",
97
+ "html",
98
+ "markdown",
99
+ "yaml",
100
+ "toml",
101
+ "shell",
102
+ "sql",
103
+ "other",
104
+ ] as const;
105
+
106
+ export const LS_GROUP = ["python", "go", "rust", "swift", "csharp"] as const;
107
+
108
+ export const EXTENSION_ID = [
109
+ "vscodevim.vim",
110
+ "tuttieee.emacs-mcx",
111
+ "ms-vscode.sublime-keybindings",
112
+ "ms-python.python",
113
+ "astral-sh.ty",
114
+ "charliermarsh.ruff",
115
+ "golang.go",
116
+ "rust-lang.rust-analyzer",
117
+ "swiftlang.swift-vscode",
118
+ "llvm-vs-code-extensions.lldb-dap",
119
+ "muhammad-sammy.csharp",
120
+ "ms-dotnettools.vscode-dotnet-runtime",
121
+ ] as const;
122
+
123
+ export const EXTENSION_TRIGGER = [
124
+ "user",
125
+ "auto_upgrade",
126
+ "startup_seed",
127
+ "keymap",
128
+ "rollback",
129
+ ] as const;
130
+
131
+ export const EXTENSION_INSTALL_PHASE = ["download", "install"] as const;
132
+
133
+ export const SETTING_NAME = [
134
+ "telemetry_enabled",
135
+ "keymap",
136
+ "software_map_enabled",
137
+ "scratchpad_enabled",
138
+ "diffr_config",
139
+ "structural_diff",
140
+ "theme",
141
+ "ctrl_tab",
142
+ "ready_notification",
143
+ "document_width",
144
+ ] as const;
145
+
146
+ export const REVIEW_OPENED_VIA = ["home", "cli", "other"] as const;
147
+
148
+ /** Where a review's source comes from: its target's kind, or the one scratchpad. */
149
+ export const SESSION_SOURCE_KIND = [
150
+ "worktree",
151
+ "commits",
152
+ "scratchpad",
153
+ ] as const;
154
+
155
+ /**
156
+ * The authoring agent a review session is attributed to. This is the one
157
+ * definition: `review-telemetry.ts`'s `ReviewSessionAgent` type imports and
158
+ * derives from it instead of redeclaring the list.
159
+ */
160
+ export const SESSION_AGENT_KIND = [
161
+ "codex",
162
+ "claude",
163
+ "cursor",
164
+ "opencode",
165
+ "pi",
166
+ "omp",
167
+ "other",
168
+ ] as const;
169
+
170
+ export type ReviewSessionAgent = (typeof SESSION_AGENT_KIND)[number];
171
+
172
+ /**
173
+ * How a review session ends. `abnormal` is reported on the next launch for a
174
+ * session the previous process never closed. This is the one definition:
175
+ * `review-telemetry.ts`'s `ReviewSessionOutcome` type imports and derives from
176
+ * it instead of redeclaring the list.
177
+ */
178
+ export const SESSION_OUTCOME = [
179
+ "closed",
180
+ "dismissed",
181
+ "deleted",
182
+ "app_quit",
183
+ "abnormal",
184
+ ] as const;
185
+
186
+ export type ReviewSessionOutcome = (typeof SESSION_OUTCOME)[number];
187
+
188
+ const REVIEW_DISMISSED_VIA = ["review_topbar", "home"] as const;
189
+
190
+ // "open" is the implicit undo: opening a dismissed review brings it back.
191
+ const REVIEW_RESTORED_VIA = ["home", "open"] as const;
192
+
193
+ const MAP_LEVEL = ["system", "container", "component", "code"] as const;
194
+
195
+ const SOURCE_TREE_OPENED_VIA = ["topbar", "home"] as const;
196
+
197
+ /** Tab names the dwell beacon reports. Shared with its request parser. */
198
+ export const REVIEW_TELEMETRY_TABS = [
199
+ "review",
200
+ "commits",
201
+ "map",
202
+ "files",
203
+ "trace",
204
+ ] as const;
205
+
206
+ const COMMIT_DIFF_VIA = ["row", "file", "footer"] as const;
207
+
208
+ export const CLIENT_ERROR_SOURCE = [
209
+ "window",
210
+ "worker",
211
+ "fetch",
212
+ "render",
213
+ "document",
214
+ "renderer_unexpected",
215
+ "main_unexpected",
216
+ "bootstrap",
217
+ // A workbench setting write the app requested failed.
218
+ "settings",
219
+ // The server utility process's own uncaught errors.
220
+ "server_unexpected",
221
+ ] as const;
222
+
223
+ /** Which process died. "unknown" is a minidump the live listeners did not see. */
224
+ export const CRASH_PROCESS = [
225
+ "renderer",
226
+ "gpu",
227
+ "utility",
228
+ "server",
229
+ "unknown",
230
+ ] as const;
231
+
232
+ export const CRASH_SOURCE = ["live", "minidump"] as const;
233
+
234
+ /** Where a packaged macOS build runs from; Squirrel cannot update `volume` or `translocated`. */
235
+ export const INSTALL_LOCATION = [
236
+ "applications",
237
+ "user_applications",
238
+ "volume",
239
+ "translocated",
240
+ "other",
241
+ ] as const;
242
+
243
+ export const DIFF_OPENED_VIA = ["topbar", "lens", "locate"] as const;
244
+
245
+ /** Which diff opened: a commit's, or a file diff drawn plain or structural. */
246
+ export const DIFF_OPENED_KIND = ["commit", "file", "structural"] as const;
247
+
248
+ export const DISCORD_VIA = ["topbar", "dialog", "docs"] as const;
249
+
250
+ const REVIEW_DELETED_VIA = ["home"] as const;
251
+
252
+ export const ERROR_PROCESS = ["main", "renderer", "canvas", "server"] as const;
253
+
254
+ export const UPDATE_FAILURE_PHASE = ["check", "download", "install"] as const;
255
+
256
+ export const UPDATE_MESSAGE_SOURCE = [
257
+ "electron",
258
+ "request",
259
+ "shipit",
260
+ "fallback",
261
+ ] as const;
262
+
263
+ /**
264
+ * Directories that exist only in the shipped Review bundle. A stack frame is
265
+ * reportable when — and only when — it has already been rewritten to start at
266
+ * one of these, which is what strips the user's home directory off the front.
267
+ * See error-telemetry.ts; this table re-checks the result independently.
268
+ */
269
+ export const BUNDLE_FRAME_ROOTS = [
270
+ "vs/review/",
271
+ "vs/workbench/",
272
+ "vs/platform/",
273
+ "vs/base/",
274
+ "vs/code/",
275
+ "vs/editor/",
276
+ "assets/",
277
+ "review-runtime/",
278
+ ] as const;
279
+
280
+ /**
281
+ * The Electron entry files, which sit beside those directories rather than
282
+ * inside one. A crash before the app can start lands here and nowhere else, so
283
+ * without this list the report that matters most carries no location at all.
284
+ * The list is closed for the same reason the directories are.
285
+ */
286
+ export const BUNDLE_FRAME_ENTRY_FILES = [
287
+ "main.js",
288
+ "cli.js",
289
+ "bootstrap-cli.js",
290
+ "bootstrap-esm.js",
291
+ "bootstrap-fork.js",
292
+ "bootstrap-meta.js",
293
+ "bootstrap-node.js",
294
+ ] as const;
295
+
296
+ /**
297
+ * One normalized stack frame: a bundle-relative path, then `:line:col`. The
298
+ * character class is deliberately narrow — anything outside it could carry user
299
+ * content — and there is no function name, because the file already identifies
300
+ * the frame and a minified name adds surface for nothing.
301
+ */
302
+ const escapeForPattern = (value: string): string =>
303
+ value.replaceAll(".", "\\.").replaceAll("/", "\\/");
304
+
305
+ export const BUNDLE_FRAME_PATTERN = new RegExp(
306
+ "^(?:" +
307
+ [
308
+ `(?:${BUNDLE_FRAME_ROOTS.map(escapeForPattern).join("|")})[A-Za-z0-9_./-]*`,
309
+ `(?:${BUNDLE_FRAME_ENTRY_FILES.map(escapeForPattern).join("|")})`,
310
+ ].join("|") +
311
+ "):\\d{1,9}:\\d{1,9}$",
312
+ );
313
+
314
+ export const BUNDLE_FRAME_SEPARATOR = "|";
315
+
316
+ const MAX_BUNDLE_FRAMES = 10;
317
+
318
+ const MAX_BUNDLE_FRAMES_LENGTH = 1_024;
319
+
320
+ const HASH_HEX_PATTERN = /^[0-9a-f]{16}$/;
321
+
322
+ const MAX_CLEANED_MESSAGE_LENGTH = 300;
323
+
324
+ /** A marker the cleaner writes, e.g. `<REDACTED: user-file-path>`. */
325
+ const REDACTION_MARKER_PATTERN = /<REDACTED: [A-Za-z][A-Za-z0-9 -]*>/g;
326
+
327
+ /**
328
+ * Header the canvas app attaches to review API requests so server-side events
329
+ * can share the UI session id. The CORS preflight allowlist in
330
+ * server/hono-http.ts must permit every header named here — the canvas runs on
331
+ * a vscode-file:// origin, so a header missing from that list fails the
332
+ * preflight and takes the whole request down with it.
333
+ */
334
+ export const REVIEW_APP_SESSION_ID_HEADER = "x-review-app-session-id";
335
+
336
+ export const UI_TELEMETRY_EVENTS = {
337
+ app_opened: {
338
+ event: "review_app_opened",
339
+ properties: {},
340
+ },
341
+ peek_opened: {
342
+ event: "review_peek_opened",
343
+ properties: { via: PEEK_VIA },
344
+ },
345
+ tour_started: {
346
+ event: "review_tour_started",
347
+ properties: { steps: "number" },
348
+ },
349
+ tour_completed: {
350
+ event: "review_tour_completed",
351
+ properties: { steps: "number" },
352
+ },
353
+ map_expanded: {
354
+ event: "review_map_expanded",
355
+ properties: { level: MAP_LEVEL },
356
+ },
357
+ commit_expanded: {
358
+ event: "review_commit_expanded",
359
+ properties: { expanded: "boolean" },
360
+ },
361
+ commit_diff_opened: {
362
+ event: "review_commit_diff_opened",
363
+ properties: { via: COMMIT_DIFF_VIA },
364
+ },
365
+ source_tree_opened: {
366
+ event: "review_source_tree_opened",
367
+ properties: { via: SOURCE_TREE_OPENED_VIA },
368
+ },
369
+ diff_layout_changed: {
370
+ event: "review_diff_layout_changed",
371
+ properties: { layout: REVIEW_DIFF_LAYOUTS },
372
+ },
373
+ // Dismissal replaced the old reject decision: the reader is finished with the
374
+ // review, whether or not they liked the change.
375
+ review_dismissed: {
376
+ event: "review_review_dismissed",
377
+ properties: { via: REVIEW_DISMISSED_VIA },
378
+ },
379
+ // The undo. It says the reader was not finished after all.
380
+ review_restored: {
381
+ event: "review_review_restored",
382
+ properties: { via: REVIEW_RESTORED_VIA },
383
+ },
384
+ client_error: {
385
+ event: "review_client_error",
386
+ // error_name is a JS error class name (TypeError, …), never a message.
387
+ // component is a stable, package-owned Review component or helper name,
388
+ // never an authored value or property path.
389
+ // message, message_hash and frames are derived server-side from the raw
390
+ // error, which travels in the request's `error` envelope and never in these
391
+ // properties.
392
+ // The property is error_source, not source: captureUiEvent stamps
393
+ // source: "review_app" on every UI event, and a property of the same name
394
+ // silently overwrites it.
395
+ properties: {
396
+ error_source: CLIENT_ERROR_SOURCE,
397
+ error_process: ERROR_PROCESS,
398
+ error_name: "enum_free_short",
399
+ component: "enum_free_short",
400
+ message: "cleaned_message",
401
+ message_hash: "hash_hex",
402
+ frames: "bundle_frames",
403
+ },
404
+ },
405
+ update_started: {
406
+ event: "review_update_started",
407
+ properties: {
408
+ update_attempt_id: "opaque_id",
409
+ target_version: "release_version",
410
+ },
411
+ },
412
+ update_completed: {
413
+ event: "review_update_completed",
414
+ properties: {
415
+ update_attempt_id: "opaque_id",
416
+ target_version: "release_version",
417
+ duration_ms: "number",
418
+ },
419
+ },
420
+ update_failed: {
421
+ event: "review_update_failed",
422
+ properties: {
423
+ phase: UPDATE_FAILURE_PHASE,
424
+ message_source: UPDATE_MESSAGE_SOURCE,
425
+ update_attempt_id: "opaque_id",
426
+ target_version: "release_version",
427
+ duration_ms: "number",
428
+ error_name: "enum_free_short",
429
+ message: "cleaned_message",
430
+ message_hash: "hash_hex",
431
+ },
432
+ },
433
+ lsp_used: {
434
+ event: "review_lsp_used",
435
+ properties: {
436
+ feature: LSP_FEATURE,
437
+ via: LSP_VIA,
438
+ language: LSP_LANGUAGE,
439
+ editor_kind: LSP_EDITOR_KIND,
440
+ },
441
+ },
442
+ ls_activated: {
443
+ event: "review_ls_activated",
444
+ properties: { group: LS_GROUP, ok: "boolean" },
445
+ },
446
+ extension_installed: {
447
+ event: "review_extension_installed",
448
+ properties: {
449
+ extension_id: EXTENSION_ID,
450
+ trigger: EXTENSION_TRIGGER,
451
+ cached: "boolean",
452
+ duration_ms: "number",
453
+ },
454
+ },
455
+ extension_install_failed: {
456
+ event: "review_extension_install_failed",
457
+ properties: {
458
+ extension_id: EXTENSION_ID,
459
+ trigger: EXTENSION_TRIGGER,
460
+ phase: EXTENSION_INSTALL_PHASE,
461
+ },
462
+ },
463
+ extension_enabled: {
464
+ event: "review_extension_enabled",
465
+ properties: { extension_id: EXTENSION_ID, trigger: EXTENSION_TRIGGER },
466
+ },
467
+ extension_disabled: {
468
+ event: "review_extension_disabled",
469
+ properties: { extension_id: EXTENSION_ID, trigger: EXTENSION_TRIGGER },
470
+ },
471
+ extension_uninstalled: {
472
+ event: "review_extension_uninstalled",
473
+ properties: { extension_id: EXTENSION_ID, trigger: EXTENSION_TRIGGER },
474
+ },
475
+ tour_step_advanced: {
476
+ event: "review_tour_step_advanced",
477
+ properties: { step: "number", steps: "number" },
478
+ },
479
+ tour_abandoned: {
480
+ event: "review_tour_abandoned",
481
+ properties: { step: "number", steps: "number" },
482
+ },
483
+ bug_report_dialog_opened: {
484
+ event: "review_bug_report_dialog_opened",
485
+ properties: {},
486
+ },
487
+ bug_report_cancelled: {
488
+ event: "review_bug_report_cancelled",
489
+ properties: {},
490
+ },
491
+ bug_report_send_failed: {
492
+ event: "review_bug_report_send_failed",
493
+ properties: { error_name: "enum_free_short" },
494
+ },
495
+ setting_changed: {
496
+ event: "review_setting_changed",
497
+ // value carries the chosen option for the one multi-valued setting, theme.
498
+ properties: {
499
+ setting: SETTING_NAME,
500
+ enabled: "boolean",
501
+ value: REVIEW_THEME_CHOICES,
502
+ },
503
+ },
504
+ review_opened: {
505
+ event: "review_review_opened",
506
+ properties: { via: REVIEW_OPENED_VIA },
507
+ },
508
+ session_started: {
509
+ event: "review_session_started",
510
+ properties: {
511
+ source_kind: SESSION_SOURCE_KIND,
512
+ agent_kind: SESSION_AGENT_KIND,
513
+ },
514
+ },
515
+ session_ended: {
516
+ event: "review_session_ended",
517
+ properties: { outcome: SESSION_OUTCOME, duration_ms: "number" },
518
+ },
519
+ review_presented: {
520
+ event: "review_review_presented",
521
+ properties: { load_ms: "number" },
522
+ },
523
+ home_empty_state_viewed: {
524
+ event: "review_home_empty_state_viewed",
525
+ properties: {},
526
+ },
527
+ crash: {
528
+ event: "review_crash",
529
+ // reason is Electron's closed reason string (oom, crashed, abnormal-exit…)
530
+ // or a signal name from the server supervisor; never a message.
531
+ properties: {
532
+ process: CRASH_PROCESS,
533
+ reason: "enum_free_short",
534
+ exit_code: "number",
535
+ uptime_ms: "number",
536
+ source: CRASH_SOURCE,
537
+ },
538
+ },
539
+ hang_started: { event: "review_hang_started", properties: {} },
540
+ hang_ended: {
541
+ event: "review_hang_ended",
542
+ properties: { duration_ms: "number" },
543
+ },
544
+ app_ready: {
545
+ event: "review_app_ready",
546
+ properties: { duration_ms: "number", install_location: INSTALL_LOCATION },
547
+ },
548
+ error_burst: {
549
+ event: "review_error_burst",
550
+ properties: { message_hash: "hash_hex", suppressed: "number" },
551
+ },
552
+ diff_opened: {
553
+ event: "review_diff_opened",
554
+ properties: { kind: DIFF_OPENED_KIND, via: DIFF_OPENED_VIA },
555
+ },
556
+ scratchpad_opened: { event: "review_scratchpad_opened", properties: {} },
557
+ discord_clicked: {
558
+ event: "review_discord_clicked",
559
+ properties: { via: DISCORD_VIA },
560
+ },
561
+ discord_dialog_shown: {
562
+ event: "review_discord_dialog_shown",
563
+ properties: {},
564
+ },
565
+ discord_dialog_dismissed: {
566
+ event: "review_discord_dialog_dismissed",
567
+ properties: {},
568
+ },
569
+ peek_resolved: {
570
+ event: "review_peek_resolved",
571
+ properties: { root_kind: PEEK_ROOT_KIND },
572
+ },
573
+ peek_resolve_failed: {
574
+ event: "review_peek_resolve_failed",
575
+ properties: { root_kind: PEEK_ROOT_KIND },
576
+ },
577
+ review_deleted: {
578
+ event: "review_review_deleted",
579
+ properties: { via: REVIEW_DELETED_VIA },
580
+ },
581
+ // The reader copied the review's share link.
582
+ review_shared: { event: "review_review_shared", properties: {} },
583
+ } as const satisfies Record<
584
+ string,
585
+ { event: string; properties: Record<string, UiTelemetryPropertySpec> }
586
+ >;
587
+
588
+ export type UiTelemetryEventName = keyof typeof UI_TELEMETRY_EVENTS;
589
+
590
+ const MAX_FREE_STRING_LENGTH = 40;
591
+
592
+ const FREE_STRING_PATTERN = /^[A-Za-z0-9_$-]+$/;
593
+
594
+ const OPAQUE_ID_PATTERN = /^[A-Za-z0-9_-]{8,64}$/;
595
+
596
+ const RELEASE_VERSION_PATTERN =
597
+ /^\d{1,10}\.\d{1,10}\.\d{1,10}(?:[-+][0-9A-Za-z.-]{1,40})?$/;
598
+
599
+ const COMMON_PROPERTIES = {
600
+ app_session_id: "opaque_id",
601
+ } as const satisfies Record<string, UiTelemetryPropertySpec>;
602
+
603
+ function isUiTelemetryEventName(name: string): name is UiTelemetryEventName {
604
+ return Object.hasOwn(UI_TELEMETRY_EVENTS, name);
605
+ }
606
+
607
+ /**
608
+ * Validate a raw UI event payload against the allowlist. Returns the PostHog
609
+ * event name plus only the sanctioned properties, or null when the event
610
+ * itself is unknown. Unknown keys and invalid values are silently dropped —
611
+ * never forwarded.
612
+ */
613
+ export function sanitizeUiTelemetryEvent(input: {
614
+ name: unknown;
615
+ properties?: unknown;
616
+ }): {
617
+ event: string;
618
+ properties: Record<string, string | number | boolean>;
619
+ } | null {
620
+ const name = z.string().safeParse(input.name);
621
+
622
+ if (!name.success || !isUiTelemetryEventName(name.data)) return null;
623
+ const spec: UiTelemetryEventSpec = UI_TELEMETRY_EVENTS[name.data];
624
+
625
+ const raw: JsonObject = isJsonObject(input.properties)
626
+ ? input.properties
627
+ : {};
628
+
629
+ const properties: Record<string, string | number | boolean> = {};
630
+
631
+ const propertySpecs: UiTelemetryEventSpec["properties"] = {
632
+ ...COMMON_PROPERTIES,
633
+ ...spec.properties,
634
+ };
635
+
636
+ for (const [key, propSpec] of Object.entries(propertySpecs)) {
637
+ const value = raw[key];
638
+
639
+ if (value === undefined || value === null) continue;
640
+ const text = jsonString(value);
641
+
642
+ if (propSpec === "number") {
643
+ const number = jsonNumber(value);
644
+
645
+ if (number !== undefined) properties[key] = number;
646
+ continue;
647
+ }
648
+
649
+ if (propSpec === "boolean") {
650
+ const boolean = jsonBoolean(value);
651
+
652
+ if (boolean !== undefined) properties[key] = boolean;
653
+ continue;
654
+ }
655
+
656
+ if (propSpec === "opaque_id") {
657
+ if (isValidReviewAppSessionId(text)) properties[key] = text;
658
+ continue;
659
+ }
660
+
661
+ if (propSpec === "release_version") {
662
+ if (text !== undefined && RELEASE_VERSION_PATTERN.test(text)) {
663
+ properties[key] = text;
664
+ }
665
+
666
+ continue;
667
+ }
668
+
669
+ if (propSpec === "enum_free_short") {
670
+ if (
671
+ text !== undefined &&
672
+ text.length <= MAX_FREE_STRING_LENGTH &&
673
+ FREE_STRING_PATTERN.test(text)
674
+ ) {
675
+ properties[key] = text;
676
+ }
677
+
678
+ continue;
679
+ }
680
+
681
+ if (propSpec === "hash_hex") {
682
+ if (text !== undefined && HASH_HEX_PATTERN.test(text)) {
683
+ properties[key] = text;
684
+ }
685
+
686
+ continue;
687
+ }
688
+
689
+ if (propSpec === "bundle_frames") {
690
+ const frames = sanitizeBundleFrames(text);
691
+
692
+ if (frames) properties[key] = frames;
693
+ continue;
694
+ }
695
+
696
+ if (propSpec === "cleaned_message") {
697
+ if (text !== undefined && isReportableCleanedMessage(text)) {
698
+ properties[key] = text;
699
+ }
700
+
701
+ continue;
702
+ }
703
+
704
+ if (text !== undefined && propSpec.includes(text)) {
705
+ properties[key] = text;
706
+ }
707
+ }
708
+
709
+ return { event: spec.event, properties };
710
+ }
711
+
712
+ /**
713
+ * The independent second check on a cleaned error message.
714
+ *
715
+ * It never repairs the message. Either the cleaner finished the job or the
716
+ * message is dropped and only the digest goes, so a bug in the cleaner cannot
717
+ * by itself put a path on the wire.
718
+ *
719
+ * The path rule is the load-bearing one: the cleaner replaces every path-shaped
720
+ * run with a marker, so a surviving path shape means something got past it.
721
+ */
722
+ export function isReportableCleanedMessage(value: string): boolean {
723
+ if (value.length === 0 || value.length > MAX_CLEANED_MESSAGE_LENGTH) {
724
+ return false;
725
+ }
726
+
727
+ // The cleaner's own markers are known-safe output, and several of them name
728
+ // the thing they replaced — "<REDACTED: GitHub Token>" holds the word "token"
729
+ // and would trip the secret rule below. So the checks apply to what is left
730
+ // once the markers are removed. The marker shape is deliberately narrow, so a
731
+ // producer cannot hide content inside a marker of its own.
732
+ const remainder = value.replaceAll(REDACTION_MARKER_PATTERN, " ");
733
+
734
+ return !containsFilePath(remainder) && !hasPossibleUserInfo(remainder);
735
+ }
736
+
737
+ /**
738
+ * Keep only the frames that match BUNDLE_FRAME_PATTERN. Frames arrive already
739
+ * normalized; this is the independent second check, so it never repairs a frame
740
+ * — it drops it. Returns undefined when nothing survives.
741
+ */
742
+ function sanitizeBundleFrames(value: string | undefined): string | undefined {
743
+ if (value === undefined) return undefined;
744
+ const kept: string[] = [];
745
+ let length = 0;
746
+
747
+ for (const frame of value.split(BUNDLE_FRAME_SEPARATOR)) {
748
+ if (!BUNDLE_FRAME_PATTERN.test(frame)) continue;
749
+ const next = length + frame.length + (kept.length > 0 ? 1 : 0);
750
+
751
+ if (next > MAX_BUNDLE_FRAMES_LENGTH) break;
752
+ kept.push(frame);
753
+ length = next;
754
+
755
+ if (kept.length >= MAX_BUNDLE_FRAMES) break;
756
+ }
757
+
758
+ return kept.length > 0 ? kept.join(BUNDLE_FRAME_SEPARATOR) : undefined;
759
+ }
760
+
761
+ export function isValidReviewAppSessionId(
762
+ value: string | undefined,
763
+ ): value is string {
764
+ return value !== undefined && OPAQUE_ID_PATTERN.test(value);
765
+ }