Repository navigation
Commit 5cefae0
fix(profile): split V1 and V2 profile sample schemas (#909)
## Summary
Fixes the ZodError surfaced in
[MCP-SERVER-FRN](https://sentry.sentry.io/issues/7420869276/) when
parsing Sentry's transaction profile responses:
```
Failed to validate keys: profile.samples.<array>.thread_id,
profile.samples.<array>.timestamp, transaction.active_thread_id
```
Sentry's profiling service (vroom) emits two distinct sample shapes, but
the previous `ProfileSampleSchema` only accepted the V2 one:
| | V1 transaction profile | V2 continuous profile chunk |
| --- | --- | --- |
| `Sample.thread_id` | `uint64` (number) | `string` |
| time field | `elapsed_since_start_ns` (uint64 ns) | `timestamp`
(seconds, float) |
| `Transaction.active_thread_id` | `uint64` (number) | n/a |
Rather than make one permissive schema accept both, this PR splits them
into discriminated schemas that each describe exactly one wire format —
an unexpected V1 payload can't silently match V2 and vice versa.
### Changes
- `schema.ts`
- New `ProfileChunkSampleSchema` (V2): strict `thread_id: string`,
required `timestamp: number`. Used by `ProfileChunkSchema`.
- New `TransactionProfileSampleSchema` (V1): `thread_id: string | number
→ string` via `.transform(String)`, **required**
`elapsed_since_start_ns: number`. No `timestamp` field — vroom's V1 wire
format never has one. Used by `TransactionProfileSchema`.
- `TransactionProfileSchema.profile` no longer reuses
`ProfileChunkSchema.shape.profile`; it gets its own `profile` object
wired to `TransactionProfileSampleSchema`.
- `TransactionProfileSchema.transaction.active_thread_id` accepts
`string | number` and normalizes to `string`.
- Factored shared `ProfileThreadMetadataSchema`.
- `types.ts` – removed `ProfileSample`, added `ProfileChunkSample` and
`TransactionProfileSample` (derived via `z.infer`).
- `formatter.ts`
- `ProfileSampleData` is now a narrow structural type (`{ stack_id;
thread_id }`) so both V1 and V2 samples flow through the shared
sample/frames rendering without coupling their timing fields.
- `getTransactionProfileDurationNs` now derives duration directly from
`elapsed_since_start_ns` after the `relative_start_ns`/`relative_end_ns`
path. The old `timestamp`-based inference and
`inferSampleTimestampNsScale` helper are gone — they were only reachable
when V1 samples carried a `timestamp`, which the wire format never does.
- Fixtures
- Renamed `profile-details.json` → `transaction-profile-v1.json` (and
the exported `profileDetailsFixture` → `transactionProfileV1Fixture`) to
make the wire format explicit at the name level. References updated
across `mcp-server-mocks` (imports, MSW handlers, barrel exports) and
all consumer tests in `mcp-core`.
- Fixture content now reflects vroom's actual V1 wire format end-to-end:
numeric `Sample.thread_id`, `elapsed_since_start_ns` timing, **and**
numeric `transaction.active_thread_id` (previously left as `"1"`, which
made the `active_thread_id` transform test trivially pass). The existing
`profile-chunk.json` continues to serve as the canonical V2 fixture.
- Tests
- New `ProfileChunkSampleSchema` tests including negative cases that
reject V1-only fields.
- New `TransactionProfileSampleSchema` tests including a negative case
that rejects samples missing `elapsed_since_start_ns` (i.e. rejects a
V2-shaped payload on the V1 path).
- Round-trip tests that parse `transaction-profile-v1.json` through
`TransactionProfileSchema` and `profile-chunk.json` through
`ProfileChunkSchema`, guarding against fixture drift.
- Formatter regression test for the V1 `elapsed_since_start_ns` duration
fallback; legacy `timestamp`-based formatter tests were updated to use
`elapsed_since_start_ns` (or removed where no longer reachable).
- `createMockTransactionProfile()` parses the fixture through
`TransactionProfileSchema.parse()` so `.transform(String)` runs on both
the sample-level and transaction-level ids (matching the production code
path; addresses Cursor Bugbot's earlier finding).
## Review & Testing Checklist for Human
- [ ] Both schemas are now strict about their respective wire formats
(V2 requires `timestamp`, V1 requires `elapsed_since_start_ns`, neither
accepts the other's time field). If any real-world payload historically
deviated from this — e.g. a V1 sample missing `elapsed_since_start_ns`,
or a V2 chunk with a numeric `thread_id` — it will now fail validation
where the old permissive schema succeeded. Confirm that's acceptable, or
relax the specific field back to `.optional()`.
- [ ] Public type rename: `ProfileSample` is gone, replaced by
`ProfileChunkSample` / `TransactionProfileSample`. The fixture export
`profileDetailsFixture` is likewise renamed to
`transactionProfileV1Fixture`. Repo-wide grep shows no remaining
internal users; double-check any downstream consumer outside this repo.
- [ ] Fixture rewrite: `transaction-profile-v1.json` is now fully
V1-shaped (numeric ids at both the sample and transaction level, ns
offsets). Evals / resource snapshots that render this fixture may need
baselines regenerated.
- [ ] Optional end-to-end: hit the actual Sentry transaction-profile
endpoint in the staging MCP server and confirm the original ZodError is
gone.
### Notes
- Quality gate locally: `tsc --noEmit` clean, `biome check` clean, full
`vitest run` in `packages/mcp-core` green (905 passed / 6 skipped).
- CI is all green on the head commit (17 checks, including Cursor Bugbot
with no findings).
- `thread_metadata` keys are strings — vroom's Go
`map[string]ThreadMetadata` serialization — which is why `thread_id` /
`active_thread_id` are normalized to string.
Link to Devin session:
https://app.devin.ai/sessions/dec3ca3b8eaf4471a837828e83fe6c25
Requested by: @dcramer
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: David Cramer <dcramer@gmail.com>1 parent 053880e commit 5cefae0
10 files changed
Lines changed: 310 additions & 114 deletions
File tree
- packages
- mcp-core/src
- api-client
- tools
- profile
- mcp-server-mocks/src
- fixtures
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
2 | 6 | | |
3 | 7 | | |
4 | 8 | | |
5 | 9 | | |
6 | 10 | | |
| 11 | + | |
| 12 | + | |
7 | 13 | | |
8 | 14 | | |
| 15 | + | |
| 16 | + | |
9 | 17 | | |
10 | 18 | | |
11 | 19 | | |
| |||
607 | 615 | | |
608 | 616 | | |
609 | 617 | | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1079 | 1079 | | |
1080 | 1080 | | |
1081 | 1081 | | |
| 1082 | + | |
| 1083 | + | |
| 1084 | + | |
| 1085 | + | |
| 1086 | + | |
| 1087 | + | |
| 1088 | + | |
| 1089 | + | |
| 1090 | + | |
1082 | 1091 | | |
1083 | | - | |
| 1092 | + | |
1084 | 1093 | | |
1085 | | - | |
1086 | | - | |
| 1094 | + | |
| 1095 | + | |
| 1096 | + | |
| 1097 | + | |
1087 | 1098 | | |
1088 | | - | |
| 1099 | + | |
1089 | 1100 | | |
1090 | 1101 | | |
1091 | 1102 | | |
| |||
1095 | 1106 | | |
1096 | 1107 | | |
1097 | 1108 | | |
1098 | | - | |
| 1109 | + | |
| 1110 | + | |
| 1111 | + | |
| 1112 | + | |
| 1113 | + | |
| 1114 | + | |
| 1115 | + | |
| 1116 | + | |
| 1117 | + | |
| 1118 | + | |
| 1119 | + | |
| 1120 | + | |
| 1121 | + | |
| 1122 | + | |
| 1123 | + | |
| 1124 | + | |
| 1125 | + | |
| 1126 | + | |
| 1127 | + | |
| 1128 | + | |
| 1129 | + | |
1099 | 1130 | | |
1100 | 1131 | | |
1101 | 1132 | | |
| |||
1116 | 1147 | | |
1117 | 1148 | | |
1118 | 1149 | | |
1119 | | - | |
| 1150 | + | |
1120 | 1151 | | |
1121 | | - | |
1122 | | - | |
1123 | | - | |
1124 | | - | |
1125 | | - | |
1126 | | - | |
1127 | | - | |
1128 | | - | |
| 1152 | + | |
1129 | 1153 | | |
1130 | 1154 | | |
1131 | 1155 | | |
| |||
1153 | 1177 | | |
1154 | 1178 | | |
1155 | 1179 | | |
| 1180 | + | |
| 1181 | + | |
| 1182 | + | |
| 1183 | + | |
| 1184 | + | |
| 1185 | + | |
| 1186 | + | |
| 1187 | + | |
1156 | 1188 | | |
1157 | 1189 | | |
1158 | 1190 | | |
| |||
1162 | 1194 | | |
1163 | 1195 | | |
1164 | 1196 | | |
1165 | | - | |
| 1197 | + | |
| 1198 | + | |
| 1199 | + | |
| 1200 | + | |
| 1201 | + | |
| 1202 | + | |
1166 | 1203 | | |
1167 | 1204 | | |
1168 | 1205 | | |
1169 | 1206 | | |
1170 | 1207 | | |
1171 | | - | |
| 1208 | + | |
| 1209 | + | |
| 1210 | + | |
| 1211 | + | |
1172 | 1212 | | |
1173 | 1213 | | |
1174 | 1214 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
49 | 48 | | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
54 | | - | |
| 49 | + | |
55 | 50 | | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
56 | 61 | | |
57 | 62 | | |
58 | 63 | | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | | - | |
64 | 64 | | |
65 | 65 | | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
66 | 70 | | |
67 | 71 | | |
68 | 72 | | |
69 | 73 | | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
70 | 77 | | |
71 | 78 | | |
72 | 79 | | |
73 | 80 | | |
| 81 | + | |
74 | 82 | | |
75 | 83 | | |
76 | 84 | | |
77 | | - | |
78 | | - | |
79 | | - | |
80 | | - | |
81 | | - | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
| 85 | + | |
| 86 | + | |
86 | 87 | | |
87 | | - | |
88 | | - | |
| 88 | + | |
| 89 | + | |
89 | 90 | | |
90 | 91 | | |
91 | 92 | | |
| |||
148 | 149 | | |
149 | 150 | | |
150 | 151 | | |
| 152 | + | |
151 | 153 | | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
152 | 157 | | |
153 | | - | |
154 | 158 | | |
155 | 159 | | |
156 | 160 | | |
| |||
0 commit comments