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
5 changes: 5 additions & 0 deletions .changeset/sentry-browser-verified.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'uptide': patch
---

The `@sentry/browser` 10 to 11 pack is verified again: 5 public upgrades, 22 real breaking sites, breaking precision 100% and recall 50%. `@sentry/react` stays a candidate: its ground truth grew to 14 public upgrades and 29 breaking sites, with 3 false positives among its breaking findings.
20 changes: 16 additions & 4 deletions .github/workflows/packs.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
name: Packs

# `uptide pack test`: fixtures, and `check` on each ground-truth repository against the
# expected findings (docs/packs.md). It fails on a false positive among breaking findings, a
# failing fixture, or a verification.json the evidence no longer supports.
# expected findings (docs/packs.md). It fails on a false positive among breaking findings (but
# for a candidate listed in FP_CANDIDATES, below), a failing fixture, or a verification.json the
# evidence no longer supports.
#
# One job per pack, in parallel: every registered pack on a push to main or a pull request that
# touches the engine, only the changed packs on a pull request that changes nothing else
Expand Down Expand Up @@ -85,11 +86,22 @@ jobs:
- name: uptide pack test ${{ matrix.pack }}
env:
PACK: ${{ matrix.pack }}
# Candidates whose ground truth has breaking false positives: still scored and shown,
# not required, but only for that reason (a candidate record, nothing unscored or left
# behind). sentry-react: 3, see packages/core/src/packs/sentry/RESEARCH.md.
FP_CANDIDATES: sentry-react
run: |
set +e
pnpm -s uptide pack test "$PACK" --json > "pack-test-$PACK.json"
code=$?
node --conditions=source --import tsx scripts/pack-test-report.ts "pack-test-$PACK.json" | tee -a "$GITHUB_STEP_SUMMARY"
node --conditions=source --import tsx scripts/pack-test-report.ts --summary "$GITHUB_STEP_SUMMARY" "pack-test-$PACK.json"
if [ "$code" -ne 0 ] && [[ " $FP_CANDIDATES " == *" $PACK "* ]] && jq -e '.packs[0]
| .status == "candidate" and .recorded == "candidate" and .breaking.falsePositives > 0
and (.problems | length) == 0 and all(.repos[]; (.error | not) and ((.leftBehind // []) | length) == 0)' \
"pack-test-$PACK.json" > /dev/null; then
echo "::warning::$PACK: a candidate with breaking false positives, not required"
exit 0
fi
exit $code
- uses: actions/cache/save@v6
if: always() && github.event_name == 'push' && steps.cache.outputs.cache-hit != 'true'
Expand Down Expand Up @@ -136,4 +148,4 @@ jobs:
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Report
if: always() && needs.scope.outputs.run == 'true'
run: node --conditions=source --import tsx scripts/pack-test-report.ts pack-test-*.json | tee -a "$GITHUB_STEP_SUMMARY"
run: node --conditions=source --import tsx scripts/pack-test-report.ts --summary "$GITHUB_STEP_SUMMARY" pack-test-*.json
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,14 @@ See the [pack compatibility matrix](docs/compatibility.md) and the [migration pa
| `react` | 18 → 19 | 8 | 100% | 69% | 130 | excalidraw/excalidraw, tldraw/tldraw | verified |
| `react-router` | 6 → 7 | 13 | 100% | 88% | 18 | Arize-ai/phoenix, getlago/lago-front | verified |
| `redis` | 4.x/5.x → 5.x/6.x | 14 | 100% | 83% | 20 | deanmoses/tacocat-gallery-sam, swoofer/mcp-coordinator, UNN-Devotek/FCM-Fallout-Chat-Mod, tearingItUp786/blog | verified |
| `@sentry/browser` | 10 → 11 | 16 | 100% | 50% | 22 | Automancer-Ltd/automancer-site, sbomify/sbomify, MihaelaAghirculesei/Pokedex, Simple-With-Us/ContactLogo, getsentry/sentry-godot | verified |
| `@sentry/nextjs` | 10 → 11 | 24 | 100% | 100% | 8 | cohstats/coh3-stats, xdoubleu/tools.xdoubleu.com, maximunited/yomu | verified |
| `stripe` | 14+ → latest, API version pinning | 5 | 100% | 92% | 21 | unkeyed/unkey, nextjs/saas-starter, starl3xx/wallet-to-social, AtlasDevHQ/atlas | verified |
| `typescript` | 5 → 6 | 12 | 100% | 99% | 1399 | m-lyon/recipe, robrechtme/recipes, JosephMaynard/dependency-radar, CroffZ/github-release-notifier, SignalK/signalk-derived-data, shogo82148/typescript-action, Borewit/music-metadata | verified |
| `vitest` | 4 → 5 | 6 | 100% | 50% | 32 | votingworks/vxsuite, vitorvasc/opentelemetry-ecosystem-explorer | verified |
| `zod` | 3 → 4 | 7 | 100% | 64% | 219 | ugurkocde/AwesomeIntune, howardyang2009/PATH, band-ai/band-sdk-typescript, vzdc-artcc/website, CANShift/canshift-core | verified |

5 more in validation: see the [compatibility matrix](docs/compatibility.md).
4 more in validation: see the [compatibility matrix](docs/compatibility.md).
<!-- packs:end -->

Any other dependency gets the same analysis without a pack, and `fix` migrates it with the
Expand Down
10 changes: 5 additions & 5 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ already run as assisted ([verification gate](packs.md)).
| `react` | 18 → 19 | 8 | 100% | 69% | 130 | excalidraw/excalidraw, tldraw/tldraw | verified |
| `react-router` | 6 → 7 | 13 | 100% | 88% | 18 | Arize-ai/phoenix, getlago/lago-front | verified |
| `redis` | 4.x/5.x → 5.x/6.x | 14 | 100% | 83% | 20 | deanmoses/tacocat-gallery-sam, swoofer/mcp-coordinator, UNN-Devotek/FCM-Fallout-Chat-Mod, tearingItUp786/blog | verified |
| `@sentry/browser` | 10 → 11 | 16 | 100% | 100% | 3 | Automancer-Ltd/automancer-site, sbomify/sbomify | candidate |
| `@sentry/browser` | 10 → 11 | 16 | 100% | 50% | 22 | Automancer-Ltd/automancer-site, sbomify/sbomify, MihaelaAghirculesei/Pokedex, Simple-With-Us/ContactLogo, getsentry/sentry-godot | verified |
| `@sentry/nextjs` | 10 → 11 | 24 | 100% | 100% | 8 | cohstats/coh3-stats, xdoubleu/tools.xdoubleu.com, maximunited/yomu | verified |
| `@sentry/react` | 10 → 11 | 16 | 100% | 100% | 2 | owine/youth-activity-scheduler, successible/cleanslate | candidate |
| `@sentry/react` | 10 → 11 | 16 | 88% | 72% | 29 | owine/youth-activity-scheduler, successible/cleanslate, Seklfreak/flimm, daler91/Hyrox-Companion, jey3dayo/ultra-rss-reader, jirikrucek/ricochet-app, louisclarencepeter/destinationparadise, poporonnet/kaniwriter, preijm/milk-me-not, CandeeGenerations/central-flock, DougZeThug/717rec, CandeeGenerations/attendance-public, cucumber/cucumber-reports, Simple-With-Us/BotFleet | candidate |
| `stripe` | 14+ → latest, API version pinning | 5 | 100% | 92% | 21 | unkeyed/unkey, nextjs/saas-starter, starl3xx/wallet-to-social, AtlasDevHQ/atlas | verified |
| `typescript` | 5 → 6 | 12 | 100% | 99% | 1399 | m-lyon/recipe, robrechtme/recipes, JosephMaynard/dependency-radar, CroffZ/github-release-notifier, SignalK/signalk-derived-data, shogo82148/typescript-action, Borewit/music-metadata | verified |
| `vitest` | 4 → 5 | 6 | 100% | 50% | 32 | votingworks/vxsuite, vitorvasc/opentelemetry-ecosystem-explorer | verified |
Expand Down Expand Up @@ -73,7 +73,7 @@ already run as assisted ([verification gate](packs.md)).
| `redis` | `renamed-exports` | 100% | 2 | 0 | |
| `redis` | `scan-iterators` | n/a | 0 | 0 | unexercised: runs assisted |
| `redis` | `unstable-resp3` | n/a | 0 | 0 | unexercised: runs assisted |
| `@sentry/browser` | `enable-logs` | 100% | 1 | 0 | |
| `@sentry/browser` | `enable-logs` | 100% | 2 | 0 | |
| `@sentry/browser` | `enable-metrics` | n/a | 0 | 0 | unexercised: runs assisted |
| `@sentry/browser` | `integration-names` | n/a | 0 | 0 | unexercised: runs assisted |
| `@sentry/browser` | `renamed-exports` | n/a | 0 | 0 | unexercised: runs assisted |
Expand All @@ -86,9 +86,9 @@ already run as assisted ([verification gate](packs.md)).
| `@sentry/nextjs` | `renamed-exports` | n/a | 0 | 0 | unexercised: runs assisted |
| `@sentry/nextjs` | `streamed-span-wrapper` | n/a | 0 | 0 | unexercised: runs assisted |
| `@sentry/nextjs` | `with-sentry-config-import` | 100% | 3 | 0 | |
| `@sentry/react` | `enable-logs` | n/a | 0 | 0 | unexercised: runs assisted |
| `@sentry/react` | `enable-logs` | 100% | 4 | 0 | |
| `@sentry/react` | `enable-metrics` | n/a | 0 | 0 | unexercised: runs assisted |
| `@sentry/react` | `integration-names` | n/a | 0 | 0 | unexercised: runs assisted |
| `@sentry/react` | `integration-names` | 0% | 0 | 1 | unexercised: runs assisted |
| `@sentry/react` | `renamed-exports` | n/a | 0 | 0 | unexercised: runs assisted |
| `@sentry/react` | `streamed-span-wrapper` | n/a | 0 | 0 | unexercised: runs assisted |
| `stripe` | `api-version-unpinned` | 100% | 5 | 0 | |
Expand Down
53 changes: 52 additions & 1 deletion packages/cli/src/pack.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type PackTestReport, registeredPacks, unfinishedPack } from '@uptide/core';
import { expect, it } from 'vitest';
import { formatPackTest } from './pack.js';
import { formatPackTest, packTestSummary, SUMMARY_LIST_LIMIT, SUMMARY_MAX_BYTES } from './pack.js';

it('prints behavior notes on their own row, n/a where nothing was predicted, and each miss with its severity', () => {
const entry = registeredPacks()[0];
Expand Down Expand Up @@ -171,3 +171,54 @@ it('prints the precision of each rewrite and warns below the threshold without f
expect(out).toContain('warning only: dom-imports below');
expect(out).toContain(`PASS ${entry.pack.name}: verified`);
});

it("keeps the job summary under GitHub's 1 MiB with every list cut, and the full lists in the log", () => {
const entry = registeredPacks()[0];
if (!entry) throw new Error('no registered pack');
const tally = {
truePositives: 0,
falsePositives: 0,
falseNegatives: 6000,
precision: 1,
recall: 0,
};
const miss = (line: number) => ({
file: `src/${'deep/'.repeat(8)}file.ts`,
line,
rule: 'generic',
});
const report = (n: number, misses = 6000): PackTestReport => ({
...unfinishedPack(entry, ''),
package: `pack-${n}`,
problems: [],
repos: [
{
label: 'owner/a-long-repository-name@0123456789ab',
from: '1.0.0',
to: '2.0.0',
predicted: [],
expected: [],
falsePositives: [],
falseNegatives: Array.from({ length: misses }, (_, i) => miss(i + 1)),
wrongRule: [],
},
],
overall: tally,
breaking: tally,
});
const packs = Array.from({ length: 20 }, (_, n) => report(n));
const MiB = 1024 * 1024;

expect(packs.map((p) => formatPackTest(p)).join('').length).toBeGreaterThan(MiB);
const summary = packTestSummary(packs, '\nAt least one pack failed.\n');
expect(Buffer.byteLength(summary)).toBeLessThan(MiB);
expect(summary).toContain('false negatives (6000)');
expect(summary).toContain(`… and ${6000 - SUMMARY_LIST_LIMIT} more, see the job log`);
expect(summary).toContain('breaking (sites)');
expect(summary.endsWith('At least one pack failed.\n')).toBe(true);

// So many packs that even the cut lists overflow: cut at a line, the fence closed.
const many = packTestSummary(Array.from({ length: 2000 }, (_, n) => report(n, 30)));
expect(Buffer.byteLength(many)).toBeLessThanOrEqual(SUMMARY_MAX_BYTES);
expect(many).toMatch(/\n… cut here to fit the job summary, see the job log\n```\n$/);
});
31 changes: 28 additions & 3 deletions packages/cli/src/pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,11 @@ function row(name: string, t: Tally): string {
return ` ${name.padEnd(28)} ${rateLabel(t, 'precision').padStart(9)} ${rateLabel(t, 'recall').padStart(7)} ${String(t.truePositives).padStart(5)} ${String(t.falsePositives).padStart(4)} ${String(t.falseNegatives).padStart(4)}`;
}

/** What `uptide pack test` prints for one pack. */
export function formatPackTest(report: PackTestReport, color = false): string {
/**
* What `uptide pack test` prints for one pack. `limit` keeps the first sites of each
* false-positive, false-negative and wrong-rule list (the job summary's version).
*/
export function formatPackTest(report: PackTestReport, color = false, limit = Infinity): string {
const c = pc.createColors(color);
const out: string[] = [];
const recorded = report.recorded === report.status ? '' : `, recorded ${report.recorded}`;
Expand Down Expand Up @@ -133,7 +136,9 @@ export function formatPackTest(report: PackTestReport, color = false): string {
if (sites.length === 0) return;
out.push('');
out.push(`${title} (${sites.length})`);
for (const s of sites) out.push(` ${s.label} ${s.file}:${s.line} ${s.text}`);
for (const s of sites.slice(0, limit))
out.push(` ${s.label} ${s.file}:${s.line} ${s.text}`);
if (sites.length > limit) out.push(` … and ${sites.length - limit} more, see the job log`);
};
listed(
'false positives',
Expand Down Expand Up @@ -220,3 +225,23 @@ export function formatPackTest(report: PackTestReport, color = false): string {
);
return `${out.join('\n')}\n`;
}

/** Sites each list keeps in the job summary; the job log has them all. */
export const SUMMARY_LIST_LIMIT = 20;
/** GitHub rejects a step summary over 1 MiB; this leaves room for the lines around the reports. */
export const SUMMARY_MAX_BYTES = 1024 * 1024 - 64 * 1024;

/**
* The reports for `$GITHUB_STEP_SUMMARY`, in a code block followed by `tail`: every score table,
* each site list cut to its first `SUMMARY_LIST_LIMIT`, and the whole cut at a line under
* `SUMMARY_MAX_BYTES` when even that is too long.
*/
export function packTestSummary(reports: PackTestReport[], tail = ''): string {
const block = (body: string) => `\`\`\`\n${body}\n\`\`\`\n${tail}`;
const body = reports.map((r) => formatPackTest(r, false, SUMMARY_LIST_LIMIT)).join('\n');
if (Buffer.byteLength(block(body)) <= SUMMARY_MAX_BYTES) return block(body);
const note = '… cut here to fit the job summary, see the job log';
const room = SUMMARY_MAX_BYTES - Buffer.byteLength(block(note));
const head = Buffer.from(body).subarray(0, Math.max(0, room)).toString('utf8');
return block(`${head.slice(0, head.lastIndexOf('\n') + 1)}${note}`);
}
152 changes: 151 additions & 1 deletion packages/core/src/packs/sentry-browser/ground-truth.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$comment": "Expectations drafted with `pnpm packs:truth` and reviewed by hand on 2026-10-14, before the pack was scored. Pure insertions (dataCollection blocks, beforeSendLog) are the decisions each repository made, not sites; comment lines that changed with an option are part of that option's site. See ../sentry/RESEARCH.md.",
"$comment": "Expectations drafted with `pnpm packs:truth` and reviewed by hand on 2026-10-14, before the pack was scored. Pure insertions (dataCollection blocks, beforeSendLog) are the decisions each repository made, not sites; comment lines that changed with an option are part of that option's site. See ../sentry/RESEARCH.md. Entries from Pokedex on were drafted and reviewed the same way on 2026-10-16 (UPT-64), before the pack was scored with them.",
"package": "@sentry/browser",
"repos": [
{
Expand Down Expand Up @@ -39,6 +39,156 @@
"note": "sendDefaultPii: true replaced by dataCollection: { userInfo: true }."
}
]
},
{
"repo": "MihaelaAghirculesei/Pokedex",
"commit": "9b48984a5c81c1bce8d0045ca545fdc1a8d6a039",
"migration": "https://github.com/MihaelaAghirculesei/Pokedex/pull/99",
"from": "10.74.0",
"to": "11.1.0",
"why": "Static site; sendDefaultPii: false replaced by dataCollection, attachStacktrace: false added in the same init (part of that change). Upgrade 3919a76399085c6c393b00d3a5413cf395ad1e55; the test asserting the init options follows the source change.",
"findings": [
{
"file": "src/monitoring.ts",
"line": 19,
"rule": "send-default-pii",
"note": "TS2353."
}
]
},
{
"repo": "Simple-With-Us/ContactLogo",
"commit": "e3ad95203065568aa148611d016353238f26eb4c",
"migration": "https://github.com/Simple-With-Us/ContactLogo/pull/161",
"directory": "web",
"from": "10.76.0",
"to": "11.4.0",
"why": "Web client; sendDefaultPii replaced by dataCollection and enableLogs: true removed from the same init. TS2353 reports only the first excess property, so enableLogs has no diagnostic. TypeScript 7.0.2 in the repository; compiled with this checkout's 5.9.3. Upgrade 4df8d7d64c764bb76b4c17cc7a1a7814bb1c5315.",
"findings": [
{
"file": "src/observability/sentry.ts",
"line": 61,
"rule": "send-default-pii",
"note": "TS2353."
},
{
"file": "src/observability/sentry.ts",
"line": 65,
"rule": "enable-logs",
"note": "enableLogs: true; no diagnostic."
}
]
},
{
"repo": "getsentry/sentry-godot",
"commit": "be1c5daf6050a1305fb40db6a10dfbfe60e46611",
"migration": "https://github.com/getsentry/sentry-godot/pull/991",
"directory": "src/sentry/javascript/bridge",
"from": "10.75.2",
"to": "11.5.0",
"with": {
"@sentry/core": "11.5.0",
"@sentry/wasm": "11.5.0"
},
"why": "The Godot SDK's web bridge on @sentry/browser, @sentry/core and @sentry/wasm, moved together by upgrade 1b67a2377c64643025889fb1dd0dc744035ea907. sendDefaultPii is passed through an any-typed options object (no diagnostic); breadcrumbsIntegration({ console }) is TS2353; beforeSendTransaction moves under traceLifecycle static; Scope.clear() is gone (TS2339). test.js (plain JS, outside the program) renames spanToStreamedSpanJSON to spanToJSON and reads the span through getActiveSpan, since ScopeData has no span in @sentry/core 11. Retyping options from any to BrowserOptions (and the orgId, beforeSend annotations it needed) compiles under v10 as well: the repository's cleanup, not sites; the import edits feed getMainCarrier and DataCollection.",
"findings": [
{
"file": "src/sentry-bridge.ts",
"line": 207,
"rule": "send-default-pii",
"note": "options typed any: no diagnostic."
},
{
"file": "src/sentry-bridge.ts",
"line": 229,
"rule": "breadcrumbs-console",
"note": "TS2353 console: false; the Console integration is excluded by name instead."
},
{
"file": "src/sentry-bridge.ts",
"line": 261,
"rule": "before-send-transaction",
"note": "Lines 261-277: kept only under traceLifecycle static."
},
{
"file": "src/sentry-bridge.ts",
"line": 312,
"rule": "removed-exports",
"note": "TS2339 Scope.clear(); comment lines 307-311 above changed with it."
},
{
"file": "src/sentry-bridge.ts",
"line": 313,
"rule": "removed-exports",
"note": "TS2339 Scope.clear()."
},
{
"file": "src/sentry-bridge.ts",
"line": 314,
"rule": "removed-exports",
"note": "TS2339 Scope.clear()."
},
{
"file": "test.js",
"line": 39,
"rule": "renamed-exports",
"note": "require of spanToStreamedSpanJSON from @sentry/core."
},
{
"file": "test.js",
"line": 418,
"rule": "renamed-exports",
"note": "spanToStreamedSpanJSON call."
},
{
"file": "test.js",
"line": 423,
"rule": "renamed-exports",
"note": "spanToStreamedSpanJSON call."
},
{
"file": "test.js",
"line": 432,
"rule": "renamed-exports",
"note": "spanToStreamedSpanJSON call."
},
{
"file": "test.js",
"line": 434,
"rule": "renamed-exports",
"note": "spanToStreamedSpanJSON call."
},
{
"file": "test.js",
"line": 436,
"rule": "renamed-exports",
"note": "spanToStreamedSpanJSON call."
},
{
"file": "test.js",
"line": 445,
"rule": "renamed-exports",
"note": "spanToStreamedSpanJSON call."
},
{
"file": "test.js",
"line": 447,
"rule": "renamed-exports",
"note": "spanToStreamedSpanJSON call."
},
{
"file": "test.js",
"line": 468,
"rule": "generic",
"note": "Lines 468-470: getScopeData().span, which @sentry/core 11 ScopeData no longer has."
},
{
"file": "test.js",
"line": 473,
"rule": "generic",
"note": "getScopeData().span."
}
]
}
]
}
Loading
Loading