react-doctor 0.9.7 → 0.9.8

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.
@@ -0,0 +1,312 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d928a2a8-2d2b-59f9-a0f9-3a81b1c49f30")}catch(e){}}();
3
+ import { D as SENTRY_FLUSH_TIMEOUT_MS, I as isDebugFlagEnabled, On as scrubSensitivePaths, U as CANONICAL_GITHUB_URL, X as DOCS_URL, en as highlighter, i as buildSentryScope, on as isPlainObject, yn as redactSensitiveText } from "./record-metric-CpxOk4TS.js";
4
+ import { o as isExpectedUserError } from "./unref-stdin-C9KAUvH3.js";
5
+ import * as Sentry from "@sentry/node";
6
+ //#region src/cli/utils/is-env-flag-enabled.ts
7
+ const isEnvFlagEnabled = (value) => value === "1" || value?.toLowerCase() === "true";
8
+ //#endregion
9
+ //#region src/cli/utils/anonymize-text.ts
10
+ /**
11
+ * Free-text fields can carry both a home-directory path (the OS username) and a
12
+ * secret/email echoed from user code, so run both scrubbers: strip the username
13
+ * from paths, then mask any known credential/PII shape. Shared by the Sentry
14
+ * event scrubber ({@link scrubSentryEvent}) and metric scrubber
15
+ * ({@link scrubSentryMetric}).
16
+ */
17
+ const anonymizeText = (text) => redactSensitiveText(scrubSensitivePaths(text));
18
+ /**
19
+ * Recursively rewrites every string within an arbitrary value (object / array /
20
+ * primitive) through {@link anonymizeText}, mutating in place. Used to sweep the
21
+ * unstructured corners of a Sentry payload (event contexts/extra/tags, breadcrumb
22
+ * data, span attributes, metric attributes) where a path or secret could hide.
23
+ */
24
+ const anonymizeInPlace = (value) => {
25
+ if (Array.isArray(value)) {
26
+ for (let index = 0; index < value.length; index += 1) {
27
+ const item = value[index];
28
+ if (typeof item === "string") value[index] = anonymizeText(item);
29
+ else anonymizeInPlace(item);
30
+ }
31
+ return;
32
+ }
33
+ if (!isPlainObject(value)) return;
34
+ for (const key of Object.keys(value)) {
35
+ const inner = value[key];
36
+ if (typeof inner === "string") value[key] = anonymizeText(inner);
37
+ else anonymizeInPlace(inner);
38
+ }
39
+ };
40
+ //#endregion
41
+ //#region src/cli/utils/scrub-sentry-event.ts
42
+ /**
43
+ * Anonymizes a Sentry event (error or transaction) before it leaves the
44
+ * machine. Strips identity the SDK attaches automatically — the IP-bearing
45
+ * `user`, the `server_name`, and the device `name` (all hostnames) — drops
46
+ * captured local variables (unbounded, un-anonymizable user data), and scrubs
47
+ * home-directory paths + known secrets/emails from every remaining string:
48
+ * messages, stack frames, breadcrumbs, contexts/extra/tags, and span
49
+ * attributes (e.g. the `inspect.directory` path on the bridged `runInspect`
50
+ * span).
51
+ *
52
+ * Wired into both `beforeSend` and `beforeSendTransaction`. If scrubbing ever
53
+ * throws, the event is dropped (`null`) rather than risk sending un-anonymized
54
+ * data — telemetry is best-effort, privacy is not.
55
+ */
56
+ const scrubSentryEvent = (event) => {
57
+ try {
58
+ delete event.server_name;
59
+ delete event.user;
60
+ const device = event.contexts?.device;
61
+ if (device) delete device.name;
62
+ if (event.contexts) anonymizeInPlace(event.contexts);
63
+ if (event.extra) anonymizeInPlace(event.extra);
64
+ if (event.tags) anonymizeInPlace(event.tags);
65
+ if (typeof event.message === "string") event.message = anonymizeText(event.message);
66
+ for (const breadcrumb of event.breadcrumbs ?? []) {
67
+ if (typeof breadcrumb.message === "string") breadcrumb.message = anonymizeText(breadcrumb.message);
68
+ if (breadcrumb.data) anonymizeInPlace(breadcrumb.data);
69
+ }
70
+ for (const exception of event.exception?.values ?? []) {
71
+ if (typeof exception.value === "string") exception.value = anonymizeText(exception.value);
72
+ for (const frame of exception.stacktrace?.frames ?? []) {
73
+ delete frame.vars;
74
+ if (typeof frame.filename === "string") frame.filename = scrubSensitivePaths(frame.filename);
75
+ if (typeof frame.abs_path === "string") frame.abs_path = scrubSensitivePaths(frame.abs_path);
76
+ if (typeof frame.module === "string") frame.module = scrubSensitivePaths(frame.module);
77
+ }
78
+ }
79
+ for (const span of event.spans ?? []) {
80
+ if (typeof span.description === "string") span.description = anonymizeText(span.description);
81
+ if (span.data) anonymizeInPlace(span.data);
82
+ }
83
+ return event;
84
+ } catch {
85
+ return null;
86
+ }
87
+ };
88
+ //#endregion
89
+ //#region src/cli/utils/scrub-sentry-metric.ts
90
+ /**
91
+ * Anonymizes a Sentry metric before it leaves the machine, mirroring
92
+ * {@link scrubSentryEvent}. Drops the `server.address` default attribute (the
93
+ * hostname) and scrubs home-directory paths + known secrets/emails from every
94
+ * remaining attribute value (metric names are our own constants, so they're
95
+ * left intact to avoid splitting a series). Returns `null` on any failure so an
96
+ * un-anonymized metric is never sent. Wired into `beforeSendMetric`.
97
+ */
98
+ const scrubSentryMetric = (metric) => {
99
+ try {
100
+ if (metric.attributes) {
101
+ delete metric.attributes["server.address"];
102
+ anonymizeInPlace(metric.attributes);
103
+ }
104
+ return metric;
105
+ } catch {
106
+ return null;
107
+ }
108
+ };
109
+ //#endregion
110
+ //#region src/cli/utils/sentry-config.ts
111
+ /**
112
+ * Shared Sentry configuration resolution — release, environment, and tracing
113
+ * sample rate — derived from `VERSION` and the standard `SENTRY_*` env
114
+ * overrides. Lives apart from `instrument.ts` (the CLI's run-context-aware
115
+ * init) so both the CLI and the editor language server can resolve identical
116
+ * release/environment values without the LSP pulling in CLI run-context code.
117
+ */
118
+ /**
119
+ * A version is a "dev" build when it's the unbuilt placeholder (`0.0.0`) or
120
+ * carries a prerelease suffix (e.g. the `-dev.<sha>` snapshots published from
121
+ * CI). Everything else is a real, tagged release.
122
+ */
123
+ const isDevVersion = (version) => version === "0.0.0" || version.includes("-");
124
+ /**
125
+ * Sentry release identifier. `react-doctor@<version>` keeps it unique within
126
+ * the org and — crucially — matches the value `scripts/sentry-sourcemaps.mjs`
127
+ * uploads source-map artifacts under, so stack frames symbolicate. Honors the
128
+ * standard `SENTRY_RELEASE` override.
129
+ */
130
+ const resolveSentryRelease = () => process.env.SENTRY_RELEASE || `react-doctor@0.9.8`;
131
+ /**
132
+ * Deployment environment shown in Sentry's environment filter. Defaults to
133
+ * `production` for tagged releases and `development` for dev/unbuilt versions,
134
+ * overridable via the standard `SENTRY_ENVIRONMENT` env var.
135
+ */
136
+ const resolveSentryEnvironment = () => process.env.SENTRY_ENVIRONMENT || (isDevVersion("0.9.8") ? "development" : "production");
137
+ /**
138
+ * Performance-tracing sample rate in `[0, 1]`. Reads `SENTRY_TRACES_SAMPLE_RATE`
139
+ * (set to `0` to disable tracing) and falls back to
140
+ * {@link SENTRY_DEFAULT_TRACES_SAMPLE_RATE}. Invalid / out-of-range values fall
141
+ * back to the default rather than silently disabling tracing.
142
+ */
143
+ const resolveTracesSampleRate = () => {
144
+ const raw = process.env.SENTRY_TRACES_SAMPLE_RATE;
145
+ if (raw === void 0 || raw.trim() === "") return 1;
146
+ const parsed = Number(raw);
147
+ if (Number.isNaN(parsed) || parsed < 0 || parsed > 1) return 1;
148
+ return parsed;
149
+ };
150
+ //#endregion
151
+ //#region src/instrument.ts
152
+ let isInitialized = false;
153
+ let resolvedTracesSampleRate = 0;
154
+ const shouldEnableSentry = () => {
155
+ if (process.argv.includes("--no-score") || process.argv.includes("--no-telemetry")) return false;
156
+ if (process.env.VITEST || process.env.NODE_ENV === "test") return false;
157
+ return true;
158
+ };
159
+ /**
160
+ * Whether performance traces will actually be recorded — Sentry is live and the
161
+ * resolved sample rate is above zero. Used to gate the per-run root span and
162
+ * the Effect→Sentry tracer bridge so they're true no-ops when tracing is off.
163
+ */
164
+ const isSentryTracingEnabled = () => Sentry.isInitialized() && resolvedTracesSampleRate > 0;
165
+ /**
166
+ * Flushes queued Sentry events (errors + transactions) before the CLI exits, so
167
+ * the success-path transaction is delivered. A no-op when Sentry was never
168
+ * initialized, and it swallows transport failures so telemetry can never mask
169
+ * the user's result.
170
+ */
171
+ const flushSentry = async () => {
172
+ if (!Sentry.isInitialized()) return;
173
+ try {
174
+ await Sentry.flush(SENTRY_FLUSH_TIMEOUT_MS);
175
+ } catch {}
176
+ };
177
+ /**
178
+ * Initializes the Sentry Node SDK for CLI crash reporting and performance
179
+ * tracing. Invoked as the first statement of the CLI entry (`cli/index.ts`) so
180
+ * the SDK's global `uncaughtException` / `unhandledRejection` handlers and OTel
181
+ * auto-instrumentation are armed before any command runs.
182
+ *
183
+ * Exported as a function rather than a bare side-effecting import because the
184
+ * package declares `"sideEffects": false`, which lets the bundler tree-shake
185
+ * side-effect-only modules. An explicit call keeps the initialization in the
186
+ * published `dist/cli.js`.
187
+ *
188
+ * Scoped to the CLI application only — the programmatic `@react-doctor/api`
189
+ * library never initializes Sentry, so importing `diagnose()` into a consumer
190
+ * app can't hijack their telemetry.
191
+ *
192
+ * Configuration is environment-overridable for self-hosting and tuning:
193
+ * `SENTRY_DSN`, `SENTRY_ENVIRONMENT`, `SENTRY_RELEASE`,
194
+ * `SENTRY_TRACES_SAMPLE_RATE` (`0` disables tracing), and `SENTRY_DEBUG`.
195
+ */
196
+ const initializeSentry = () => {
197
+ if (isInitialized || !shouldEnableSentry()) return;
198
+ isInitialized = true;
199
+ resolvedTracesSampleRate = isDebugFlagEnabled() ? 1 : resolveTracesSampleRate();
200
+ const { tags, contexts } = buildSentryScope();
201
+ try {
202
+ Sentry.init({
203
+ dsn: process.env.SENTRY_DSN || "https://f253d570240a59b8dbd77b7a548ef133@o4510226365743104.ingest.us.sentry.io/4511487817809920",
204
+ release: resolveSentryRelease(),
205
+ environment: resolveSentryEnvironment(),
206
+ sendDefaultPii: false,
207
+ tracesSampleRate: resolvedTracesSampleRate,
208
+ debug: isEnvFlagEnabled(process.env.SENTRY_DEBUG),
209
+ initialScope: {
210
+ tags,
211
+ contexts
212
+ },
213
+ beforeSend: (event, hint) => isExpectedUserError(hint.originalException) ? null : scrubSentryEvent(event),
214
+ beforeSendTransaction: (event) => scrubSentryEvent(event),
215
+ beforeSendMetric: (metric) => scrubSentryMetric(metric)
216
+ });
217
+ } catch {
218
+ resolvedTracesSampleRate = 0;
219
+ }
220
+ };
221
+ //#endregion
222
+ //#region src/cli/utils/active-scan-abort-registry.ts
223
+ const activeScanAbortControllers = /* @__PURE__ */ new Set();
224
+ const activeScanAbortRegistry = {
225
+ register: (controller) => {
226
+ activeScanAbortControllers.add(controller);
227
+ return () => activeScanAbortControllers.delete(controller);
228
+ },
229
+ abortAll: () => {
230
+ const controllers = [...activeScanAbortControllers];
231
+ activeScanAbortControllers.clear();
232
+ for (const controller of controllers) controller.abort();
233
+ }
234
+ };
235
+ //#endregion
236
+ //#region src/cli/utils/resolve-measure-width.ts
237
+ const resolveClampedWidth = (input) => {
238
+ const terminalColumns = process.stdout.columns;
239
+ if (!terminalColumns || terminalColumns <= 0) return input.fullWidth;
240
+ const availableColumns = terminalColumns - input.reservedColumns;
241
+ return Math.max(input.minWidth, Math.min(input.fullWidth, availableColumns));
242
+ };
243
+ const resolveMeasureWidth = (reservedColumns = 0) => resolveClampedWidth({
244
+ reservedColumns,
245
+ fullWidth: 60,
246
+ minWidth: 24
247
+ });
248
+ //#endregion
249
+ //#region src/cli/utils/wrap-indented-text.ts
250
+ const wrapLine = (lineText, width, breakLongWords) => {
251
+ if (lineText.length <= width) return [lineText];
252
+ const wrappedLines = [];
253
+ let remainingText = lineText.trim();
254
+ while (remainingText.length > width) {
255
+ const candidateText = remainingText.slice(0, width);
256
+ const breakIndex = candidateText.lastIndexOf(" ");
257
+ if (breakIndex <= 0) {
258
+ if (breakLongWords) {
259
+ wrappedLines.push(candidateText);
260
+ remainingText = remainingText.slice(width).trimStart();
261
+ continue;
262
+ }
263
+ const nextSpace = remainingText.indexOf(" ");
264
+ if (nextSpace === -1) break;
265
+ wrappedLines.push(remainingText.slice(0, nextSpace));
266
+ remainingText = remainingText.slice(nextSpace + 1).trimStart();
267
+ continue;
268
+ }
269
+ wrappedLines.push(remainingText.slice(0, breakIndex));
270
+ remainingText = remainingText.slice(breakIndex + 1).trimStart();
271
+ }
272
+ if (remainingText.length > 0) wrappedLines.push(remainingText);
273
+ return wrappedLines;
274
+ };
275
+ /**
276
+ * Greedy word-wrap to a character width, returning one string per line
277
+ * (no indentation applied). Preserves existing `\n` boundaries.
278
+ */
279
+ const wrapTextToWidth = (text, width, options = {}) => {
280
+ if (width <= 0) return text.split("\n");
281
+ const breakLongWords = options.breakLongWords ?? true;
282
+ return text.split("\n").flatMap((lineText) => wrapLine(lineText, width, breakLongWords));
283
+ };
284
+ //#endregion
285
+ //#region src/cli/utils/build-footer-description-lines.ts
286
+ const FOOTER_DESCRIPTION_INDENT = " ";
287
+ const buildFooterDescriptionLines = (description) => {
288
+ return wrapTextToWidth(description, resolveMeasureWidth(2)).map((line) => highlighter.dim(`${FOOTER_DESCRIPTION_INDENT}${line}`));
289
+ };
290
+ //#endregion
291
+ //#region src/cli/utils/build-footer-link-lines.ts
292
+ const buildFooterLinkLines = ({ shareUrl }) => [
293
+ ...shareUrl === null ? [] : [
294
+ ` ${highlighter.bold("Share:")} ${highlighter.info(shareUrl)}`,
295
+ ...buildFooterDescriptionLines("Tell others how you did on socials"),
296
+ ""
297
+ ],
298
+ ` ${highlighter.bold("Docs:")} ${highlighter.info(DOCS_URL)}`,
299
+ ...buildFooterDescriptionLines("Learn more about fixing issues, setting up CI/CD, and configuring rules with a config file"),
300
+ "",
301
+ ` ${highlighter.bold("GitHub:")} ${highlighter.info(CANONICAL_GITHUB_URL)}`,
302
+ ...buildFooterDescriptionLines("Report issues and star the repository!")
303
+ ];
304
+ //#endregion
305
+ //#region src/cli/utils/build-section-divider.ts
306
+ const DIVIDER_INDENT = " ";
307
+ const buildSectionDivider = () => highlighter.dim(`${DIVIDER_INDENT}${"─".repeat(resolveMeasureWidth(2))}`);
308
+ //#endregion
309
+ export { initializeSentry as a, flushSentry as i, buildFooterLinkLines as n, isSentryTracingEnabled as o, activeScanAbortRegistry as r, buildSectionDivider as t };
310
+
311
+ //# sourceMappingURL=build-section-divider-D6YhaO90.js.map
312
+ //# debugId=d928a2a8-2d2b-59f9-a0f9-3a81b1c49f30