@sentry/react 10.36.0 → 10.38.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.
- package/build/cjs/hoist-non-react-statics.js +4 -4
- package/build/cjs/hoist-non-react-statics.js.map +1 -1
- package/build/cjs/reactrouter-compat-utils/instrumentation.js +159 -30
- package/build/cjs/reactrouter-compat-utils/instrumentation.js.map +1 -1
- package/build/cjs/reactrouter-compat-utils/lazy-routes.js +19 -10
- package/build/cjs/reactrouter-compat-utils/lazy-routes.js.map +1 -1
- package/build/esm/hoist-non-react-statics.js +4 -4
- package/build/esm/hoist-non-react-statics.js.map +1 -1
- package/build/esm/package.json +1 -1
- package/build/esm/reactrouter-compat-utils/instrumentation.js +160 -31
- package/build/esm/reactrouter-compat-utils/instrumentation.js.map +1 -1
- package/build/esm/reactrouter-compat-utils/lazy-routes.js +20 -11
- package/build/esm/reactrouter-compat-utils/lazy-routes.js.map +1 -1
- package/build/esm/reactrouter.js +1 -1
- package/build/esm/reactrouterv6.js +1 -1
- package/build/types/reactrouter-compat-utils/instrumentation.d.ts.map +1 -1
- package/build/types/reactrouter-compat-utils/lazy-routes.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { browserTracingIntegration, WINDOW, startBrowserTracingPageLoadSpan, startBrowserTracingNavigationSpan } from '@sentry/browser';
|
|
2
|
-
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, debug, spanToJSON, getCurrentScope, getClient
|
|
2
|
+
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, debug, addNonEnumerableProperty, spanToJSON, getCurrentScope, getClient } from '@sentry/core';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { DEBUG_BUILD } from '../debug-build.js';
|
|
5
5
|
import { hoistNonReactStatics } from '../hoist-non-react-statics.js';
|
|
@@ -33,6 +33,9 @@ const allRoutes = new Set();
|
|
|
33
33
|
// Tracks lazy route loads to wait before finalizing span names
|
|
34
34
|
const pendingLazyRouteLoads = new WeakMap();
|
|
35
35
|
|
|
36
|
+
// Tracks deferred lazy route promises that can be resolved when patchRoutesOnNavigation is called
|
|
37
|
+
const deferredLazyRouteResolvers = new WeakMap();
|
|
38
|
+
|
|
36
39
|
/**
|
|
37
40
|
* Schedules a callback using requestAnimationFrame when available (browser),
|
|
38
41
|
* or falls back to setTimeout for SSR environments (Node.js, createMemoryRouter tests).
|
|
@@ -158,6 +161,34 @@ function trackLazyRouteLoad(span, promise) {
|
|
|
158
161
|
});
|
|
159
162
|
}
|
|
160
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Creates a deferred promise for a span that will be resolved when patchRoutesOnNavigation is called.
|
|
166
|
+
* This ensures that patchedEnd waits for patchRoutesOnNavigation to be called before ending the span.
|
|
167
|
+
*/
|
|
168
|
+
function createDeferredLazyRoutePromise(span) {
|
|
169
|
+
const deferredPromise = new Promise(resolve => {
|
|
170
|
+
deferredLazyRouteResolvers.set(span, resolve);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
trackLazyRouteLoad(span, deferredPromise);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Resolves the deferred lazy route promise for a span.
|
|
178
|
+
* Called when patchRoutesOnNavigation is invoked.
|
|
179
|
+
*/
|
|
180
|
+
function resolveDeferredLazyRoutePromise(span) {
|
|
181
|
+
const resolver = deferredLazyRouteResolvers.get(span);
|
|
182
|
+
if (resolver) {
|
|
183
|
+
resolver();
|
|
184
|
+
deferredLazyRouteResolvers.delete(span);
|
|
185
|
+
// Clear the flag so patchSpanEnd doesn't wait unnecessarily for routes that have already loaded
|
|
186
|
+
if ((span ).__sentry_may_have_lazy_routes__) {
|
|
187
|
+
(span ).__sentry_may_have_lazy_routes__ = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
161
192
|
/**
|
|
162
193
|
* Processes resolved routes by adding them to allRoutes and checking for nested async handlers.
|
|
163
194
|
* When capturedSpan is provided, updates that specific span instead of the current active span.
|
|
@@ -378,10 +409,30 @@ function createV6CompatibleWrapCreateBrowserRouter
|
|
|
378
409
|
}
|
|
379
410
|
}
|
|
380
411
|
|
|
381
|
-
|
|
412
|
+
// Capture the active span BEFORE creating the router.
|
|
413
|
+
// This is important because the span might end (due to idle timeout) before
|
|
414
|
+
// patchRoutesOnNavigation is called by React Router.
|
|
415
|
+
const activeRootSpan = getActiveRootSpan();
|
|
416
|
+
|
|
417
|
+
// If patchRoutesOnNavigation is provided and we have an active span,
|
|
418
|
+
// mark the span as having potential lazy routes and create a deferred promise.
|
|
419
|
+
const hasPatchRoutesOnNavigation =
|
|
420
|
+
opts && 'patchRoutesOnNavigation' in opts && typeof opts.patchRoutesOnNavigation === 'function';
|
|
421
|
+
if (hasPatchRoutesOnNavigation && activeRootSpan) {
|
|
422
|
+
// Mark the span as potentially having lazy routes
|
|
423
|
+
addNonEnumerableProperty(
|
|
424
|
+
activeRootSpan ,
|
|
425
|
+
'__sentry_may_have_lazy_routes__',
|
|
426
|
+
true,
|
|
427
|
+
);
|
|
428
|
+
createDeferredLazyRoutePromise(activeRootSpan);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Pass the captured span to wrapPatchRoutesOnNavigation so it uses the same span
|
|
432
|
+
// even if the span has ended by the time patchRoutesOnNavigation is called.
|
|
433
|
+
const wrappedOpts = wrapPatchRoutesOnNavigation(opts, false, activeRootSpan);
|
|
382
434
|
const router = createRouterFunction(routes, wrappedOpts);
|
|
383
435
|
const basename = opts?.basename;
|
|
384
|
-
const activeRootSpan = getActiveRootSpan();
|
|
385
436
|
|
|
386
437
|
if (router.state.historyAction === 'POP' && activeRootSpan) {
|
|
387
438
|
updatePageloadTransaction({
|
|
@@ -431,7 +482,23 @@ function createV6CompatibleWrapCreateMemoryRouter
|
|
|
431
482
|
}
|
|
432
483
|
}
|
|
433
484
|
|
|
434
|
-
|
|
485
|
+
// Capture the active span BEFORE creating the router (same as browser router)
|
|
486
|
+
const memoryActiveRootSpanEarly = getActiveRootSpan();
|
|
487
|
+
|
|
488
|
+
// If patchRoutesOnNavigation is provided and we have an active span,
|
|
489
|
+
// mark the span as having potential lazy routes and create a deferred promise.
|
|
490
|
+
const hasPatchRoutesOnNavigation =
|
|
491
|
+
opts && 'patchRoutesOnNavigation' in opts && typeof opts.patchRoutesOnNavigation === 'function';
|
|
492
|
+
if (hasPatchRoutesOnNavigation && memoryActiveRootSpanEarly) {
|
|
493
|
+
addNonEnumerableProperty(
|
|
494
|
+
memoryActiveRootSpanEarly ,
|
|
495
|
+
'__sentry_may_have_lazy_routes__',
|
|
496
|
+
true,
|
|
497
|
+
);
|
|
498
|
+
createDeferredLazyRoutePromise(memoryActiveRootSpanEarly);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const wrappedOpts = wrapPatchRoutesOnNavigation(opts, true, memoryActiveRootSpanEarly);
|
|
435
502
|
|
|
436
503
|
const router = createRouterFunction(routes, wrappedOpts);
|
|
437
504
|
const basename = opts?.basename;
|
|
@@ -624,10 +691,10 @@ function createV6CompatibleWrapUseRoutes(origUseRoutes, version) {
|
|
|
624
691
|
return React.createElement(SentryRoutes, { routes: routes, locationArg: locationArg,} );
|
|
625
692
|
};
|
|
626
693
|
}
|
|
627
|
-
|
|
628
694
|
function wrapPatchRoutesOnNavigation(
|
|
629
695
|
opts,
|
|
630
696
|
isMemoryRouter = false,
|
|
697
|
+
capturedSpan,
|
|
631
698
|
) {
|
|
632
699
|
if (!opts || !('patchRoutesOnNavigation' in opts) || typeof opts.patchRoutesOnNavigation !== 'function') {
|
|
633
700
|
return opts || {};
|
|
@@ -640,24 +707,58 @@ function wrapPatchRoutesOnNavigation(
|
|
|
640
707
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
641
708
|
const targetPath = (args )?.path;
|
|
642
709
|
|
|
643
|
-
|
|
710
|
+
// Use current active span if available, otherwise fall back to captured span (from router creation time).
|
|
711
|
+
// This ensures navigation spans use their own span (not the stale pageload span), while still
|
|
712
|
+
// supporting pageload spans that may have ended before patchRoutesOnNavigation is called.
|
|
713
|
+
const activeRootSpan = getActiveRootSpan() ?? capturedSpan;
|
|
644
714
|
|
|
645
715
|
if (!isMemoryRouter) {
|
|
646
716
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
647
717
|
const originalPatch = (args )?.patch;
|
|
718
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
719
|
+
const matches = (args )?.matches ;
|
|
648
720
|
if (originalPatch) {
|
|
649
721
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
650
722
|
(args ).patch = (routeId, children) => {
|
|
651
723
|
addRoutesToAllRoutes(children);
|
|
652
|
-
|
|
653
|
-
//
|
|
724
|
+
|
|
725
|
+
// Find the parent route from matches and attach children to it in allRoutes.
|
|
726
|
+
// React Router's patch attaches children to its internal route copies, but we need
|
|
727
|
+
// to update the route objects in our allRoutes Set for proper route matching.
|
|
728
|
+
if (matches && matches.length > 0) {
|
|
729
|
+
const leafMatch = matches[matches.length - 1];
|
|
730
|
+
const leafRoute = leafMatch?.route;
|
|
731
|
+
if (leafRoute) {
|
|
732
|
+
// Find the matching route in allRoutes by id, reference, or path
|
|
733
|
+
const matchingRoute = Array.from(allRoutes).find(route => {
|
|
734
|
+
const idMatches = route.id !== undefined && route.id === routeId;
|
|
735
|
+
const referenceMatches = route === leafRoute;
|
|
736
|
+
const pathMatches =
|
|
737
|
+
route.path !== undefined && leafRoute.path !== undefined && route.path === leafRoute.path;
|
|
738
|
+
|
|
739
|
+
return idMatches || referenceMatches || pathMatches;
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
if (matchingRoute) {
|
|
743
|
+
addResolvedRoutesToParent(children, matchingRoute);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Use the captured activeRootSpan instead of getActiveRootSpan() to avoid race conditions
|
|
749
|
+
// where user navigates away during lazy route loading and we'd update the wrong span
|
|
750
|
+
const spanJson = activeRootSpan ? spanToJSON(activeRootSpan) : undefined;
|
|
751
|
+
// Only update if we have a valid targetPath (patchRoutesOnNavigation can be called without path),
|
|
752
|
+
// the captured span exists, hasn't ended, and is a navigation span
|
|
654
753
|
if (
|
|
655
754
|
targetPath &&
|
|
656
|
-
|
|
657
|
-
|
|
755
|
+
activeRootSpan &&
|
|
756
|
+
spanJson &&
|
|
757
|
+
!spanJson.timestamp && // Span hasn't ended yet
|
|
758
|
+
spanJson.op === 'navigation'
|
|
658
759
|
) {
|
|
659
760
|
updateNavigationSpan(
|
|
660
|
-
|
|
761
|
+
activeRootSpan,
|
|
661
762
|
{ pathname: targetPath, search: '', hash: '', state: null, key: 'default' },
|
|
662
763
|
Array.from(allRoutes),
|
|
663
764
|
true,
|
|
@@ -677,15 +778,29 @@ function wrapPatchRoutesOnNavigation(
|
|
|
677
778
|
result = await originalPatchRoutes(args);
|
|
678
779
|
} finally {
|
|
679
780
|
clearNavigationContext(contextToken);
|
|
781
|
+
// Resolve the deferred promise now that patchRoutesOnNavigation has completed.
|
|
782
|
+
// This ensures patchedEnd has waited long enough for the lazy routes to load.
|
|
783
|
+
if (activeRootSpan) {
|
|
784
|
+
resolveDeferredLazyRoutePromise(activeRootSpan);
|
|
785
|
+
}
|
|
680
786
|
}
|
|
681
787
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
788
|
+
// Use the captured activeRootSpan instead of getActiveRootSpan() to avoid race conditions
|
|
789
|
+
// where user navigates away during lazy route loading and we'd update the wrong span
|
|
790
|
+
const spanJson = activeRootSpan ? spanToJSON(activeRootSpan) : undefined;
|
|
791
|
+
if (
|
|
792
|
+
activeRootSpan &&
|
|
793
|
+
spanJson &&
|
|
794
|
+
!spanJson.timestamp && // Span hasn't ended yet
|
|
795
|
+
spanJson.op === 'navigation'
|
|
796
|
+
) {
|
|
797
|
+
// Use targetPath consistently - don't fall back to WINDOW.location which may have changed
|
|
798
|
+
// if the user navigated away during async loading
|
|
799
|
+
const pathname = targetPath;
|
|
685
800
|
|
|
686
801
|
if (pathname) {
|
|
687
802
|
updateNavigationSpan(
|
|
688
|
-
|
|
803
|
+
activeRootSpan,
|
|
689
804
|
{ pathname, search: '', hash: '', state: null, key: 'default' },
|
|
690
805
|
Array.from(allRoutes),
|
|
691
806
|
false,
|
|
@@ -806,7 +921,7 @@ function handleNavigation(opts
|
|
|
806
921
|
pathname: location.pathname,
|
|
807
922
|
locationKey,
|
|
808
923
|
});
|
|
809
|
-
patchSpanEnd(navigationSpan, location, routes, basename,
|
|
924
|
+
patchSpanEnd(navigationSpan, location, routes, basename, 'navigation');
|
|
810
925
|
} else {
|
|
811
926
|
// If no span was created, remove the placeholder
|
|
812
927
|
activeNavigationSpans.delete(client);
|
|
@@ -873,8 +988,13 @@ function updatePageloadTransaction({
|
|
|
873
988
|
activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
|
|
874
989
|
|
|
875
990
|
// Patch span.end() to ensure we update the name one last time before the span is sent
|
|
876
|
-
patchSpanEnd(activeRootSpan, location, routes, basename,
|
|
991
|
+
patchSpanEnd(activeRootSpan, location, routes, basename, 'pageload');
|
|
877
992
|
}
|
|
993
|
+
} else if (activeRootSpan) {
|
|
994
|
+
// Even if branches is null (can happen when lazy routes haven't loaded yet),
|
|
995
|
+
// we still need to patch span.end() so that when lazy routes load and the span ends,
|
|
996
|
+
// we can update the transaction name correctly.
|
|
997
|
+
patchSpanEnd(activeRootSpan, location, routes, basename, 'pageload');
|
|
878
998
|
}
|
|
879
999
|
}
|
|
880
1000
|
|
|
@@ -969,7 +1089,6 @@ function patchSpanEnd(
|
|
|
969
1089
|
location,
|
|
970
1090
|
routes,
|
|
971
1091
|
basename,
|
|
972
|
-
_allRoutes,
|
|
973
1092
|
spanType,
|
|
974
1093
|
) {
|
|
975
1094
|
const patchedPropertyName = `__sentry_${spanType}_end_patched__` ;
|
|
@@ -979,8 +1098,7 @@ function patchSpanEnd(
|
|
|
979
1098
|
return;
|
|
980
1099
|
}
|
|
981
1100
|
|
|
982
|
-
//
|
|
983
|
-
const allRoutesSet = _allRoutes ? new Set(_allRoutes) : allRoutes;
|
|
1101
|
+
// Uses global allRoutes to access lazy-loaded routes added after this function was called.
|
|
984
1102
|
|
|
985
1103
|
const originalEnd = span.end.bind(span);
|
|
986
1104
|
let endCalled = false;
|
|
@@ -1011,29 +1129,40 @@ function patchSpanEnd(
|
|
|
1011
1129
|
};
|
|
1012
1130
|
|
|
1013
1131
|
const pendingPromises = pendingLazyRouteLoads.get(span);
|
|
1132
|
+
const mayHaveLazyRoutes = (span ).__sentry_may_have_lazy_routes__;
|
|
1133
|
+
|
|
1014
1134
|
// Wait for lazy routes if:
|
|
1015
|
-
// 1. There are pending promises AND
|
|
1135
|
+
// 1. (There are pending promises OR the span was marked as potentially having lazy routes) AND
|
|
1016
1136
|
// 2. Current name exists AND
|
|
1017
1137
|
// 3. Either the name has a wildcard OR the source is not 'route' (URL-based names)
|
|
1138
|
+
const hasPendingOrMayHaveLazyRoutes = (pendingPromises && pendingPromises.size > 0) || mayHaveLazyRoutes;
|
|
1018
1139
|
const shouldWaitForLazyRoutes =
|
|
1019
|
-
|
|
1020
|
-
pendingPromises.size > 0 &&
|
|
1140
|
+
hasPendingOrMayHaveLazyRoutes &&
|
|
1021
1141
|
currentName &&
|
|
1022
1142
|
(transactionNameHasWildcard(currentName) || currentSource !== 'route');
|
|
1023
1143
|
|
|
1024
1144
|
if (shouldWaitForLazyRoutes) {
|
|
1025
1145
|
if (_lazyRouteTimeout === 0) {
|
|
1026
|
-
tryUpdateSpanNameBeforeEnd(span, spanJson, currentName, location, routes, basename, spanType,
|
|
1146
|
+
tryUpdateSpanNameBeforeEnd(span, spanJson, currentName, location, routes, basename, spanType, allRoutes);
|
|
1027
1147
|
cleanupNavigationSpan();
|
|
1028
1148
|
originalEnd(endTimestamp);
|
|
1029
1149
|
return;
|
|
1030
1150
|
}
|
|
1031
1151
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1152
|
+
// If we have pending promises, wait for them. Otherwise, just wait for the timeout.
|
|
1153
|
+
// This handles the case where we know lazy routes might load but patchRoutesOnNavigation
|
|
1154
|
+
// hasn't been called yet.
|
|
1155
|
+
const timeoutPromise = new Promise(r => setTimeout(r, _lazyRouteTimeout));
|
|
1156
|
+
let waitPromise;
|
|
1157
|
+
|
|
1158
|
+
if (pendingPromises && pendingPromises.size > 0) {
|
|
1159
|
+
const allSettled = Promise.allSettled(pendingPromises).then(() => {});
|
|
1160
|
+
waitPromise = _lazyRouteTimeout === Infinity ? allSettled : Promise.race([allSettled, timeoutPromise]);
|
|
1161
|
+
} else {
|
|
1162
|
+
// No pending promises yet, but we know lazy routes might load
|
|
1163
|
+
// Wait for the timeout to give React Router time to call patchRoutesOnNavigation
|
|
1164
|
+
waitPromise = timeoutPromise;
|
|
1165
|
+
}
|
|
1037
1166
|
|
|
1038
1167
|
waitPromise
|
|
1039
1168
|
.then(() => {
|
|
@@ -1046,7 +1175,7 @@ function patchSpanEnd(
|
|
|
1046
1175
|
routes,
|
|
1047
1176
|
basename,
|
|
1048
1177
|
spanType,
|
|
1049
|
-
|
|
1178
|
+
allRoutes,
|
|
1050
1179
|
);
|
|
1051
1180
|
cleanupNavigationSpan();
|
|
1052
1181
|
originalEnd(endTimestamp);
|
|
@@ -1058,7 +1187,7 @@ function patchSpanEnd(
|
|
|
1058
1187
|
return;
|
|
1059
1188
|
}
|
|
1060
1189
|
|
|
1061
|
-
tryUpdateSpanNameBeforeEnd(span, spanJson, currentName, location, routes, basename, spanType,
|
|
1190
|
+
tryUpdateSpanNameBeforeEnd(span, spanJson, currentName, location, routes, basename, spanType, allRoutes);
|
|
1062
1191
|
cleanupNavigationSpan();
|
|
1063
1192
|
originalEnd(endTimestamp);
|
|
1064
1193
|
};
|