Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- feat(sveltekit): Import `sentrySvelteKit` from `@sentry/sveltekit/vite` on SDK v11 and write `sentryUrl` for self-hosted ([#1348](https://github.com/getsentry/sentry-wizard/pull/1348))
- feat(cloudflare): Write `nodejs_compat` instead of `nodejs_als` to the wrangler config and drop a stale `nodejs_als` ([#1353](https://github.com/getsentry/sentry-wizard/pull/1353))
- feat(js): Warn when the wizard runs on a Node.js version that SDK v11 does not support ([#1354](https://github.com/getsentry/sentry-wizard/pull/1354))
- feat(js): Add shared dataCollection prompt and preset (v11 only) ([#1358](https://github.com/getsentry/sentry-wizard/pull/1358))
- fix(sourcemaps): Treat prerelease SDK versions as up to date ([#1345](https://github.com/getsentry/sentry-wizard/pull/1345))

## 7.0.3
Expand Down
18 changes: 3 additions & 15 deletions src/react-router/sdk-version.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { major, minVersion } from 'semver';
import { getMajorVersion } from '../utils/semver';

/**
* Version range of `@sentry/react-router` (and `@sentry/profiling-node`) the
Expand Down Expand Up @@ -27,23 +27,11 @@ export function getSentryReactRouterVitePluginImportPath(
installedSdkVersion: string | undefined,
): string {
const sdkMajor =
getMajor(installedSdkVersion) ??
getMajor(SENTRY_REACT_ROUTER_SDK_RANGE) ??
getMajorVersion(installedSdkVersion) ??
getMajorVersion(SENTRY_REACT_ROUTER_SDK_RANGE) ??
0;

return sdkMajor >= 11
? SENTRY_REACT_ROUTER_VITE_IMPORT_PATH
: SENTRY_REACT_ROUTER_ROOT_IMPORT_PATH;
}

function getMajor(version: string | undefined): number | undefined {
if (!version) {
return undefined;
}
try {
const minVer = minVersion(version);
return minVer ? major(minVer) : undefined;
} catch {
return undefined;
}
}
20 changes: 6 additions & 14 deletions src/sveltekit/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { lt, major, minVersion } from 'semver';
import { lt, minVersion } from 'semver';

import { getMajorVersion } from '../utils/semver';

/**
* Version range of `@sentry/sveltekit` the wizard installs. Also decides the
Expand All @@ -22,25 +24,15 @@ export function getSentrySvelteKitVitePluginImportPath(
installedSdkVersion: string | undefined,
): string {
const sdkMajor =
getMajor(installedSdkVersion) ?? getMajor(SENTRY_SVELTEKIT_SDK_RANGE) ?? 0;
getMajorVersion(installedSdkVersion) ??
getMajorVersion(SENTRY_SVELTEKIT_SDK_RANGE) ??
0;

return sdkMajor >= 11
? SENTRY_SVELTEKIT_VITE_IMPORT_PATH
: SENTRY_SVELTEKIT_ROOT_IMPORT_PATH;
}

function getMajor(version: string | undefined): number | undefined {
if (!version) {
return undefined;
}
try {
const minVer = minVersion(version);
return minVer ? major(minVer) : undefined;
} catch {
return undefined;
}
}

export type KitVersionBucket =
| 'none'
| 'invalid'
Expand Down
116 changes: 116 additions & 0 deletions src/utils/data-collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// @ts-expect-error - clack is ESM and TS complains about that. It works though
import * as clack from '@clack/prompts';
import * as Sentry from '@sentry/node';

import { traceStep } from '../telemetry';
import { abortIfCancelled } from './clack';
import { getMajorVersion } from './semver';

export const DATA_COLLECTION_DOCS_URL =
'https://docs.sentry.io/platforms/javascript/configuration/options/#dataCollection';

/**
* Keys that commonly reveal user identity in HTTP headers, cookies, and URL
* query parameters (forwarding chains, IP addresses, user hints).
*/
const PII_KEY_DENYLIST = ['forwarded', '-ip', 'remote-', 'via', '-user'];

const PII_KEY_DENYLIST_SNIPPET = `[${PII_KEY_DENYLIST.map(
(key) => `"${key}"`,
).join(', ')}]`;

/**
* `dataCollection` value the wizard writes when the user opts to reduce data
* collection. Kept as an object so magicast-based wizards can insert it as an
* AST node; `getDataCollectionSnippet` renders the same preset for
* template-string-based wizards.
*/
export const DATA_COLLECTION_PII_PRESET = {
userInfo: false,
graphQL: { document: false, variables: false },
genAI: { inputs: false, outputs: false },
databaseQueryData: false,
queues: false,
httpBodies: [],
httpHeaders: { deny: [...PII_KEY_DENYLIST] },
cookies: { deny: [...PII_KEY_DENYLIST] },
urlQueryParams: { deny: [...PII_KEY_DENYLIST] },
};

export const DATA_COLLECTION_COMMENT_LINES = [
'Turns off collection of data that could identify users. Adjust per category:',
DATA_COLLECTION_DOCS_URL,
];

/**
* Renders the PII preset (with its explanatory comment) as code to embed in a
* generated `Sentry.init` options object. Every line is prefixed with
* `indent`; the result ends without a trailing newline.
*/
export function getDataCollectionSnippet(indent = ' '): string {
const lines = [
...DATA_COLLECTION_COMMENT_LINES.map((line) => `// ${line}`),
'dataCollection: {',
' userInfo: false,',
' graphQL: { document: false, variables: false },',
' genAI: { inputs: false, outputs: false },',
' databaseQueryData: false,',
' queues: false,',
' httpBodies: [],',
` httpHeaders: { deny: ${PII_KEY_DENYLIST_SNIPPET} },`,
` cookies: { deny: ${PII_KEY_DENYLIST_SNIPPET} },`,
` urlQueryParams: { deny: ${PII_KEY_DENYLIST_SNIPPET} },`,
'},',
];

return lines.map((line) => `${indent}${line}`).join('\n');
}

/**
* The `dataCollection` option only makes sense to offer from SDK v11 on,
* where the SDK collects rich context by default. Unknown or unparseable
* installed versions fall back to the major of the range the wizard installs.
*/
export function sdkSupportsDataCollection(
installedSdkVersion: string | undefined,
wizardInstallRange: string,
): boolean {
const sdkMajor =
getMajorVersion(installedSdkVersion) ??
getMajorVersion(wizardInstallRange) ??
0;

return sdkMajor >= 11;
}

/**
* Asks whether the wizard should write the PII-reducing `dataCollection`
* preset. Call this only when `sdkSupportsDataCollection` returned `true`.
*/
export async function askShouldReduceDataCollection(): Promise<boolean> {
return traceStep('data-collection', async () => {
const reduceDataCollection: boolean = await abortIfCancelled(
clack.select({
message:
'Sentry collects request data, user info, and other context by default. Do you want to reduce this to avoid sending personally identifiable information (PII)?',
initialValue: false,
options: [
{
value: true,
label: 'Yes',
hint: 'Add a dataCollection config to Sentry.init() that turns off PII-heavy categories',
},
{
value: false,
label: 'No',
hint: 'Keep the SDK defaults',
},
],
}),
);

Sentry.setTag('data-collection-reduced', reduceDataCollection);

return reduceDataCollection;
});
}
28 changes: 27 additions & 1 deletion src/utils/semver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,30 @@
import { satisfies, subset, valid, validRange } from 'semver';
import {
major,
minVersion,
satisfies,
subset,
valid,
validRange,
} from 'semver';

/**
* Returns the major version of an exact version or the lowest version that a
* range permits (e.g. `^10` -> 10, `~11.2.0` -> 11). Returns `undefined` for
* missing or unparseable input.
*/
export function getMajorVersion(
version: string | undefined,
): number | undefined {
if (!version) {
return undefined;
}
try {
const minVer = minVersion(version);
return minVer ? major(minVer) : undefined;
} catch {
return undefined;
}
}

export function fulfillsVersionRange({
version,
Expand Down
92 changes: 92 additions & 0 deletions test/utils/data-collection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, expect, it } from 'vitest';

import {
getDataCollectionSnippet,
sdkSupportsDataCollection,
} from '../../src/utils/data-collection';
import { getMajorVersion } from '../../src/utils/semver';

describe('getMajorVersion', () => {
it.each([
['^10', 10],
['^10.73.0', 10],
['~11.2.0', 11],
['11.0.0', 11],
// Prerelease versions keep their major, so v11 rc/beta installs pass the gate
['11.0.0-rc.1', 11],
['^11.0.0-rc.0', 11],
['~11.0.0-beta.2', 11],
['>=10.57.0 <12', 10],
])('returns the lowest permitted major for %s', (version, expected) => {
expect(getMajorVersion(version)).toBe(expected);
});

it.each([undefined, '', 'not-a-version', 'workspace:*'])(
'returns undefined for %s',
(version) => {
expect(getMajorVersion(version)).toBeUndefined();
},
);
});

describe('sdkSupportsDataCollection', () => {
it.each([
['^11.0.0', '^10'],
['11.2.3', '^10'],
['^12', '^10'],
['11.0.0-rc.1', '^10'],
['^11.0.0-beta.0', '^10'],
])('returns true for installed version %s', (installed, range) => {
expect(sdkSupportsDataCollection(installed, range)).toBe(true);
});

it.each([
['^10.73.0', '^10'],
['10.99.0', '^10'],
['^9', '^10'],
['10.99.0-rc.1', '^10'],
])('returns false for installed version %s', (installed, range) => {
expect(sdkSupportsDataCollection(installed, range)).toBe(false);
});

it('falls back to the wizard install range when the installed version is missing', () => {
expect(sdkSupportsDataCollection(undefined, '^11')).toBe(true);
expect(sdkSupportsDataCollection(undefined, '^10')).toBe(false);
});

it('falls back to the wizard install range when the installed version is unparseable', () => {
expect(sdkSupportsDataCollection('workspace:*', '^11')).toBe(true);
expect(sdkSupportsDataCollection('workspace:*', '^10')).toBe(false);
});

it('returns false when neither version is parseable', () => {
expect(sdkSupportsDataCollection(undefined, 'not-a-version')).toBe(false);
});
});

describe('getDataCollectionSnippet', () => {
it('renders the preset with the default indentation', () => {
expect(getDataCollectionSnippet()).toMatchInlineSnapshot(`
" // Turns off collection of data that could identify users. Adjust per category:
// https://docs.sentry.io/platforms/javascript/configuration/options/#dataCollection
dataCollection: {
userInfo: false,
graphQL: { document: false, variables: false },
genAI: { inputs: false, outputs: false },
databaseQueryData: false,
queues: false,
httpBodies: [],
httpHeaders: { deny: ["forwarded", "-ip", "remote-", "via", "-user"] },
cookies: { deny: ["forwarded", "-ip", "remote-", "via", "-user"] },
urlQueryParams: { deny: ["forwarded", "-ip", "remote-", "via", "-user"] },
},"
`);
});

it('prefixes every line with the given indent', () => {
const snippet = getDataCollectionSnippet(' ');
for (const line of snippet.split('\n')) {
expect(line.startsWith(' ')).toBe(true);
}
});
});
Loading