diff --git a/CHANGELOG.md b/CHANGELOG.md index 66a4c1e425..04ce51f32a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,59 @@ Access to Mobile Replay is limited to early access orgs on Sentry. If you're interested, [sign up for the waitlist](https://sentry.io/lp/mobile-replay-beta/) +## 5.24.1 + +### Fixes + +- App Start Native Frames can start with zeroed values ([#3881](https://github.com/getsentry/sentry-react-native/pull/3881)) + +### Dependencies + +- Bump Cocoa SDK from v8.28.0 to v8.29.1 ([#3890](https://github.com/getsentry/sentry-react-native/pull/3890)) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8291) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.28.0...8.29.1) + +## 5.24.0 + +### Features + +- Add native application start spans ([#3855](https://github.com/getsentry/sentry-react-native/pull/3855), [#3884](https://github.com/getsentry/sentry-react-native/pull/3884)) + - This doesn't change the app start measurement length, but add child spans (more detail) into the existing app start span +- Added JS Bundle Execution start information to the application start measurements ([#3857](https://github.com/getsentry/sentry-react-native/pull/3857)) + +### Fixes + +- Add more expressive debug logs to Native Frames Integration ([#3880](https://github.com/getsentry/sentry-react-native/pull/3880)) +- Add missing tracing integrations when using `client.init()` ([#3882](https://github.com/getsentry/sentry-react-native/pull/3882)) +- Ensure `sentry-cli` doesn't trigger Xcode `error:` prefix ([#3887](https://github.com/getsentry/sentry-react-native/pull/3887)) + - Fixes `--allow-failure` failing Xcode builds + +### Dependencies + +- Bump Cocoa SDK from v8.27.0 to v8.28.0 ([#3866](https://github.com/getsentry/sentry-react-native/pull/3866)) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8280) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.27.0...8.28.0) +- Bump Android SDK from v7.8.0 to v7.10.0 ([#3805](https://github.com/getsentry/sentry-react-native/pull/3805)) + - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#7100) + - [diff](https://github.com/getsentry/sentry-java/compare/7.8.0...7.10.0) +- Bump JavaScript SDK from v7.113.0 to v7.117.0 ([#3806](https://github.com/getsentry/sentry-react-native/pull/3806)) + - [changelog](https://github.com/getsentry/sentry-javascript/blob/v7/CHANGELOG.md#71170) + - [diff](https://github.com/getsentry/sentry-javascript/compare/7.113.0...7.117.0) + +## 5.23.1 + +### Fixes + +- Fix failing iOS builds due to missing SentryLevel ([#3854](https://github.com/getsentry/sentry-react-native/pull/3854)) +- Add missing logs to dropped App Start spans ([#3861](https://github.com/getsentry/sentry-react-native/pull/3861)) +- Make all options of `startTimeToInitialDisplaySpan` optional ([#3867](https://github.com/getsentry/sentry-react-native/pull/3867)) +- Add Span IDs to Time to Display debug logs ([#3868](https://github.com/getsentry/sentry-react-native/pull/3868)) +- Use TTID end timestamp when TTFD should be updated with an earlier timestamp ([#3869](https://github.com/getsentry/sentry-react-native/pull/3869)) + +## 5.23.0 + +This release does *not* build on iOS. Please use `5.23.1` or newer. + ### Features - Functional integrations ([#3814](https://github.com/getsentry/sentry-react-native/pull/3814)) @@ -638,7 +691,7 @@ This release is compatible with `expo@50.0.0-preview.6` and newer. }); ``` - Read more at https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#7690 + Read more at - Report current screen in `contexts.app.view_names` ([#3339](https://github.com/getsentry/sentry-react-native/pull/3339)) @@ -1029,6 +1082,7 @@ This has been fixed in [version `5.9.1`](https://github.com/getsentry/sentry-rea ## 5.4.0 ### Features + - Add TS 4.1 typings ([#2995](https://github.com/getsentry/sentry-react-native/pull/2995)) - TS 3.8 are present and work automatically with older projects - Add CPU Info to Device Context ([#2984](https://github.com/getsentry/sentry-react-native/pull/2984)) @@ -2676,7 +2730,7 @@ We are looking into ways making this more stable and plan to re-enable it again ## v0.23.2 -- Fixed #228 again ¯\\_(ツ)_/¯ +- Fixed #228 again ¯\\*(ツ)*/¯ ## v0.23.1 diff --git a/RNSentry.podspec b/RNSentry.podspec index 393a8f6171..c0bc242be2 100644 --- a/RNSentry.podspec +++ b/RNSentry.podspec @@ -33,7 +33,7 @@ Pod::Spec.new do |s| s.preserve_paths = '*.js' s.dependency 'React-Core' - s.dependency 'Sentry/HybridSDK', '8.27.0' + s.dependency 'Sentry/HybridSDK', '8.29.1' s.source_files = 'ios/**/*.{h,m,mm}' s.public_header_files = 'ios/RNSentry.h' diff --git a/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index dc1b56e0b3..a5f58926c2 100644 --- a/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 330F308C2C0F3840002A0D4E /* RNSentryBreadcrumbTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 330F308B2C0F3840002A0D4E /* RNSentryBreadcrumbTests.m */; }; 33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */; }; 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */; }; 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */; }; @@ -16,6 +17,8 @@ /* Begin PBXFileReference section */ 1482D5685A340AB93348A43D /* Pods-RNSentryCocoaTesterTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNSentryCocoaTesterTests.release.xcconfig"; path = "Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests.release.xcconfig"; sourceTree = ""; }; + 330F308B2C0F3840002A0D4E /* RNSentryBreadcrumbTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryBreadcrumbTests.m; sourceTree = ""; }; + 330F308D2C0F385A002A0D4E /* RNSentryBreadcrumb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryBreadcrumb.h; path = ../ios/RNSentryBreadcrumb.h; sourceTree = ""; }; 3360898D29524164007C7730 /* RNSentryCocoaTesterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RNSentryCocoaTesterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 338739072A7D7D2800950DDD /* RNSentryTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryTests.h; sourceTree = ""; }; 33958C672BFCEF5A00AD1FB6 /* RNSentryOnDrawReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryOnDrawReporter.h; path = ../ios/RNSentryOnDrawReporter.h; sourceTree = ""; }; @@ -80,6 +83,7 @@ 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */, 33AFDFF22B8D15F600AAB120 /* RNSentryDependencyContainerTests.h */, 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */, + 330F308B2C0F3840002A0D4E /* RNSentryBreadcrumbTests.m */, ); path = RNSentryCocoaTesterTests; sourceTree = ""; @@ -87,6 +91,7 @@ 33AFE0122B8F319000AAB120 /* RNSentry */ = { isa = PBXGroup; children = ( + 330F308D2C0F385A002A0D4E /* RNSentryBreadcrumb.h */, 33958C672BFCEF5A00AD1FB6 /* RNSentryOnDrawReporter.h */, 33AFE0132B8F31AF00AAB120 /* RNSentryDependencyContainer.h */, ); @@ -204,6 +209,7 @@ 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */, 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */, 33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */, + 330F308C2C0F3840002A0D4E /* RNSentryBreadcrumbTests.m in Sources */, 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryBreadcrumbTests.m b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryBreadcrumbTests.m new file mode 100644 index 0000000000..f9609f06eb --- /dev/null +++ b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryBreadcrumbTests.m @@ -0,0 +1,40 @@ +#import +#import "RNSentryBreadcrumb.h" +@import Sentry; + +@interface RNSentryBreadcrumbTests : XCTestCase + +@end + +@implementation RNSentryBreadcrumbTests + +- (void)testGeneratesSentryBreadcrumbFromNSDictionary +{ + SentryBreadcrumb* actualCrumb = [RNSentryBreadcrumb from:@{ + @"level": @"error", + @"category": @"testCategory", + @"type": @"testType", + @"message": @"testMessage", + @"data": @{ + @"test": @"data" + } + }]; + + XCTAssertEqual(actualCrumb.level, kSentryLevelError); + XCTAssertEqual(actualCrumb.category, @"testCategory"); + XCTAssertEqual(actualCrumb.type, @"testType"); + XCTAssertEqual(actualCrumb.message, @"testMessage"); + XCTAssertTrue([actualCrumb.data isKindOfClass:[NSDictionary class]]); + XCTAssertEqual(actualCrumb.data[@"test"], @"data"); +} + +- (void)testUsesInfoAsDefaultSentryLevel +{ + SentryBreadcrumb* actualCrumb = [RNSentryBreadcrumb from:@{ + @"message": @"testMessage", + }]; + + XCTAssertEqual(actualCrumb.level, kSentryLevelInfo); +} + +@end diff --git a/android/build.gradle b/android/build.gradle index e0b8091552..2e6789c46c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -54,5 +54,5 @@ android { dependencies { implementation 'com.facebook.react:react-native:+' - api 'io.sentry:sentry-android:7.9.0-alpha.1' + api 'io.sentry:sentry-android:7.11.0-alpha.2' } diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 7bc43cfbb6..179e6dd9d5 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -105,7 +105,7 @@ public class RNSentryModuleImpl { private FrameMetricsAggregator frameMetricsAggregator = null; private boolean androidXAvailable; - private static boolean didFetchAppStart; + private static boolean hasFetchedAppStart; // 700ms to constitute frozen frames. private static final int FROZEN_FRAME_THRESHOLD = 700; @@ -369,28 +369,17 @@ public void fetchNativeRelease(Promise promise) { } public void fetchNativeAppStart(Promise promise) { - final AppStartMetrics appStartInstance = AppStartMetrics.getInstance(); - final SentryDate appStartTime = appStartInstance.getAppStartTimeSpan().getStartTimestamp(); - final boolean isColdStart = appStartInstance.getAppStartType() == AppStartMetrics.AppStartType.COLD; + final Map measurement = InternalSentrySdk.getAppStartMeasurement(); - if (appStartTime == null) { - logger.log(SentryLevel.WARNING, "App start won't be sent due to missing appStartTime."); - promise.resolve(null); - } else { - final double appStartTimestampMs = DateUtils.nanosToMillis(appStartTime.nanoTimestamp()); - - WritableMap appStart = Arguments.createMap(); - - appStart.putDouble("appStartTime", appStartTimestampMs); - appStart.putBoolean("isColdStart", isColdStart); - appStart.putBoolean("didFetchAppStart", didFetchAppStart); + WritableMap mutableMeasurement = (WritableMap) RNSentryMapConverter.convertToWritable(measurement); + mutableMeasurement.putBoolean("has_fetched", hasFetchedAppStart); - promise.resolve(appStart); - } // This is always set to true, as we would only allow an app start fetch to only // happen once in the case of a JS bundle reload, we do not want it to be // instrumented again. - didFetchAppStart = true; + hasFetchedAppStart = true; + + promise.resolve(mutableMeasurement); } /** @@ -427,11 +416,6 @@ public void fetchNativeFrames(Promise promise) { } } - if (totalFrames == 0 && slowFrames == 0 && frozenFrames == 0) { - promise.resolve(null); - return; - } - WritableMap map = Arguments.createMap(); map.putInt("totalFrames", totalFrames); map.putInt("slowFrames", slowFrames); diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index 79da19826b..f6dbcbba74 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -23,6 +23,7 @@ #import #import #import "RNSentryId.h" +#import "RNSentryBreadcrumb.h" // This guard prevents importing Hermes in JSC apps #if SENTRY_PROFILING_ENABLED @@ -54,7 +55,7 @@ + (void)storeEnvelope:(SentryEnvelope *)envelope; @end -static bool didFetchAppStart; +static bool hasFetchedAppStart; static NSString* const nativeSdkName = @"sentry.cocoa.react-native"; @@ -400,24 +401,20 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAdd rejecter:(RCTPromiseRejectBlock)reject) { #if SENTRY_HAS_UIKIT - SentryAppStartMeasurement *appStartMeasurement = PrivateSentrySDKOnly.appStartMeasurement; - - if (appStartMeasurement == nil) { + NSDictionary *measurements = [PrivateSentrySDKOnly appStartMeasurementWithSpans]; + if (measurements == nil) { resolve(nil); - } else { - BOOL isColdStart = appStartMeasurement.type == SentryAppStartTypeCold; - - resolve(@{ - @"isColdStart": [NSNumber numberWithBool:isColdStart], - @"appStartTime": [NSNumber numberWithDouble:(appStartMeasurement.appStartTimestamp.timeIntervalSince1970 * 1000)], - @"didFetchAppStart": [NSNumber numberWithBool:didFetchAppStart], - }); - + return; } + NSMutableDictionary *mutableMeasurements = [[NSMutableDictionary alloc] initWithDictionary:measurements]; + [mutableMeasurements setValue:[NSNumber numberWithBool:hasFetchedAppStart] forKey:@"has_fetched"]; + // This is always set to true, as we would only allow an app start fetch to only happen once // in the case of a JS bundle reload, we do not want it to be instrumented again. - didFetchAppStart = true; + hasFetchedAppStart = true; + + resolve(mutableMeasurements); #else resolve(nil); #endif @@ -439,12 +436,6 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAdd NSNumber *total = [NSNumber numberWithLong:frames.total]; NSNumber *frozen = [NSNumber numberWithLong:frames.frozen]; NSNumber *slow = [NSNumber numberWithLong:frames.slow]; - NSNumber *zero = [NSNumber numberWithLong:0L]; - - if ([total isEqualToNumber:zero] && [frozen isEqualToNumber:zero] && [slow isEqualToNumber:zero]) { - resolve(nil); - return; - } resolve(@{ @"totalFrames": total, @@ -578,32 +569,7 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAdd RCT_EXPORT_METHOD(addBreadcrumb:(NSDictionary *)breadcrumb) { [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { - SentryBreadcrumb* breadcrumbInstance = [[SentryBreadcrumb alloc] init]; - - NSString * levelString = breadcrumb[@"level"]; - SentryLevel sentryLevel; - if ([levelString isEqualToString:@"fatal"]) { - sentryLevel = kSentryLevelFatal; - } else if ([levelString isEqualToString:@"warning"]) { - sentryLevel = kSentryLevelWarning; - } else if ([levelString isEqualToString:@"error"]) { - sentryLevel = kSentryLevelError; - } else if ([levelString isEqualToString:@"debug"]) { - sentryLevel = kSentryLevelDebug; - } else { - sentryLevel = kSentryLevelInfo; - } - [breadcrumbInstance setLevel:sentryLevel]; - - [breadcrumbInstance setCategory:breadcrumb[@"category"]]; - - [breadcrumbInstance setType:breadcrumb[@"type"]]; - - [breadcrumbInstance setMessage:breadcrumb[@"message"]]; - - [breadcrumbInstance setData:breadcrumb[@"data"]]; - - [scope addBreadcrumb:breadcrumbInstance]; + [scope addBreadcrumb:[RNSentryBreadcrumb from:breadcrumb]]; }]; } diff --git a/ios/RNSentryBreadcrumb.h b/ios/RNSentryBreadcrumb.h new file mode 100644 index 0000000000..35e5501ae1 --- /dev/null +++ b/ios/RNSentryBreadcrumb.h @@ -0,0 +1,9 @@ +#import + +@class SentryBreadcrumb; + +@interface RNSentryBreadcrumb : NSObject + ++ (SentryBreadcrumb *)from: (NSDictionary *) dict; + +@end diff --git a/ios/RNSentryBreadcrumb.m b/ios/RNSentryBreadcrumb.m new file mode 100644 index 0000000000..928033e99c --- /dev/null +++ b/ios/RNSentryBreadcrumb.m @@ -0,0 +1,33 @@ +#import "RNSentryBreadcrumb.h" +@import Sentry; + +@implementation RNSentryBreadcrumb + ++(SentryBreadcrumb*) from: (NSDictionary *) dict +{ + SentryBreadcrumb* crumb = [[SentryBreadcrumb alloc] init]; + + NSString * levelString = dict[@"level"]; + SentryLevel sentryLevel; + if ([levelString isEqualToString:@"fatal"]) { + sentryLevel = kSentryLevelFatal; + } else if ([levelString isEqualToString:@"warning"]) { + sentryLevel = kSentryLevelWarning; + } else if ([levelString isEqualToString:@"error"]) { + sentryLevel = kSentryLevelError; + } else if ([levelString isEqualToString:@"debug"]) { + sentryLevel = kSentryLevelDebug; + } else { + sentryLevel = kSentryLevelInfo; + } + + [crumb setLevel:sentryLevel]; + [crumb setCategory:dict[@"category"]]; + [crumb setType:dict[@"type"]]; + [crumb setMessage:dict[@"message"]]; + [crumb setData:dict[@"data"]]; + + return crumb; +} + +@end diff --git a/package.json b/package.json index dd0e9c9a54..b038aae5bf 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@sentry/react-native", "homepage": "https://github.com/getsentry/sentry-react-native", "repository": "https://github.com/getsentry/sentry-react-native", - "version": "5.23.0-alpha.1", + "version": "5.22.3", "description": "Official Sentry SDK for react-native", "typings": "dist/js/index.d.ts", "types": "dist/js/index.d.ts", @@ -67,22 +67,22 @@ "react-native": ">=0.65.0" }, "dependencies": { - "@sentry/browser": "7.113.0", + "@sentry/browser": "7.117.0", "@sentry/cli": "2.31.2", - "@sentry/core": "7.113.0", - "@sentry/hub": "7.113.0", - "@sentry/integrations": "7.113.0", - "@sentry/react": "7.113.0", - "@sentry/types": "7.113.0", - "@sentry/utils": "7.113.0" + "@sentry/core": "7.117.0", + "@sentry/hub": "7.117.0", + "@sentry/integrations": "7.117.0", + "@sentry/react": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" }, "devDependencies": { "@babel/core": "^7.23.5", "@expo/metro-config": "0.17.5", "@mswjs/interceptors": "^0.25.15", - "@sentry-internal/eslint-config-sdk": "7.113.0", - "@sentry-internal/eslint-plugin-sdk": "7.113.0", - "@sentry-internal/typescript": "7.113.0", + "@sentry-internal/eslint-config-sdk": "7.117.0", + "@sentry-internal/eslint-plugin-sdk": "7.117.0", + "@sentry-internal/typescript": "7.117.0", "@sentry/wizard": "3.16.3", "@types/jest": "^29.5.3", "@types/node": "^20.9.3", diff --git a/samples/expo/app.json b/samples/expo/app.json index 47bfff57d1..421f47ba8f 100644 --- a/samples/expo/app.json +++ b/samples/expo/app.json @@ -19,7 +19,7 @@ "ios": { "supportsTablet": true, "bundleIdentifier": "io.sentry.expo.sample", - "buildNumber": "7" + "buildNumber": "11" }, "android": { "adaptiveIcon": { @@ -27,7 +27,7 @@ "backgroundColor": "#ffffff" }, "package": "io.sentry.expo.sample", - "versionCode": 7 + "versionCode": 11 }, "web": { "bundler": "metro", @@ -59,4 +59,4 @@ ] ] } -} \ No newline at end of file +} diff --git a/samples/react-native/ios/sentryreactnativesample/Info.plist b/samples/react-native/ios/sentryreactnativesample/Info.plist index e6d7979cf7..7e2aa7d9d2 100644 --- a/samples/react-native/ios/sentryreactnativesample/Info.plist +++ b/samples/react-native/ios/sentryreactnativesample/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 5.23.0 + 5.24.1 CFBundleSignature ???? CFBundleVersion - 14 + 18 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/samples/react-native/ios/sentryreactnativesampleTests/Info.plist b/samples/react-native/ios/sentryreactnativesampleTests/Info.plist index 2ecb12ad02..795471a3bc 100644 --- a/samples/react-native/ios/sentryreactnativesampleTests/Info.plist +++ b/samples/react-native/ios/sentryreactnativesampleTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 5.23.0 + 5.24.1 CFBundleSignature ???? CFBundleVersion - 14 + 18 diff --git a/scripts/sentry-xcode-debug-files.sh b/scripts/sentry-xcode-debug-files.sh index 8fcc403fbd..236a4df7d1 100755 --- a/scripts/sentry-xcode-debug-files.sh +++ b/scripts/sentry-xcode-debug-files.sh @@ -35,5 +35,14 @@ if [ "$SENTRY_DISABLE_AUTO_UPLOAD" == true ]; then elif echo "$XCODE_BUILD_CONFIGURATION" | grep -iq "debug"; then # case insensitive check for "debug" echo "Skipping debug files upload for *Debug* configuration" else - /bin/sh -c "\"$LOCAL_NODE_BINARY\" $UPLOAD_DEBUG_FILES" + # 'warning:' triggers a warning in Xcode, 'error:' triggers an error + set +x +e # disable printing commands otherwise we might print `error:` by accident and allow continuing on error + SENTRY_UPLOAD_COMMAND_OUTPUT=$(/bin/sh -c "\"$LOCAL_NODE_BINARY\" $UPLOAD_DEBUG_FILES" 2>&1) + if [ $? -eq 0 ]; then + echo "$SENTRY_UPLOAD_COMMAND_OUTPUT" | awk '{print "output: sentry-cli - " $0}' + else + echo "error: sentry-cli - To disable native debug files auto upload, set SENTRY_DISABLE_AUTO_UPLOAD=true in your environment variables. Or to allow failing upload, set SENTRY_ALLOW_FAILURE=true" + echo "error: sentry-cli - $SENTRY_UPLOAD_COMMAND_OUTPUT" + fi + set -x -e # re-enable fi diff --git a/scripts/sentry-xcode.sh b/scripts/sentry-xcode.sh index 3752345c3d..e36fa74e03 100755 --- a/scripts/sentry-xcode.sh +++ b/scripts/sentry-xcode.sh @@ -23,7 +23,16 @@ ARGS="$NO_AUTO_RELEASE $SENTRY_CLI_EXTRA_ARGS $SENTRY_CLI_RN_XCODE_EXTRA_ARGS" REACT_NATIVE_XCODE_WITH_SENTRY="\"$SENTRY_CLI_EXECUTABLE\" react-native xcode $ARGS \"$REACT_NATIVE_XCODE\"" if [ "$SENTRY_DISABLE_AUTO_UPLOAD" != true ]; then - /bin/sh -c "\"$LOCAL_NODE_BINARY\" $REACT_NATIVE_XCODE_WITH_SENTRY" + # 'warning:' triggers a warning in Xcode, 'error:' triggers an error + set +x +e # disable printing commands otherwise we might print `error:` by accident and allow continuing on error + SENTRY_XCODE_COMMAND_OUTPUT=$(/bin/sh -c "\"$LOCAL_NODE_BINARY\" $REACT_NATIVE_XCODE_WITH_SENTRY" 2>&1) + if [ $? -eq 0 ]; then + echo "$SENTRY_XCODE_COMMAND_OUTPUT" | awk '{print "output: sentry-cli - " $0}' + else + echo "error: sentry-cli - To disable source maps auto upload, set SENTRY_DISABLE_AUTO_UPLOAD=true in your environment variables. Or to allow failing upload, set SENTRY_ALLOW_FAILURE=true" + echo "error: sentry-cli - $SENTRY_XCODE_COMMAND_OUTPUT" + fi + set -x -e # re-enable else echo "SENTRY_DISABLE_AUTO_UPLOAD=true, skipping sourcemaps upload" /bin/sh -c "$REACT_NATIVE_XCODE" diff --git a/scripts/update-javascript.sh b/scripts/update-javascript.sh index ca464d280f..a9cd10b23a 100755 --- a/scripts/update-javascript.sh +++ b/scripts/update-javascript.sh @@ -3,7 +3,7 @@ set -euo pipefail tagPrefix='' repo="https://github.com/getsentry/sentry-javascript.git" -packages=('@sentry/browser' '@sentry/core' '@sentry/hub' '@sentry/integrations' '@sentry/react' '@sentry/types' '@sentry/utils' '@sentry-internal/typescript') +packages=('@sentry/browser' '@sentry/core' '@sentry/react' '@sentry/types' '@sentry/utils' '@sentry-internal/typescript') packages+=('@sentry-internal/eslint-config-sdk' '@sentry-internal/eslint-plugin-sdk') . $(dirname "$0")/update-package-json.sh diff --git a/src/js/NativeRNSentry.ts b/src/js/NativeRNSentry.ts index 0c06d90331..73cdaa375e 100644 --- a/src/js/NativeRNSentry.ts +++ b/src/js/NativeRNSentry.ts @@ -92,9 +92,14 @@ export type NativeStackFrames = { }; export type NativeAppStartResponse = { - isColdStart: boolean; - appStartTime: number; - didFetchAppStart: boolean; + type: 'cold' | 'warm' | 'unknown'; + has_fetched: boolean; + app_start_timestamp_ms?: number; + spans: { + description: string; + start_timestamp_ms: number; + end_timestamp_ms: number; + }[]; }; export type NativeFramesResponse = { diff --git a/src/js/client.ts b/src/js/client.ts index 57a9673316..81e84fc8fa 100644 --- a/src/js/client.ts +++ b/src/js/client.ts @@ -107,13 +107,6 @@ export class ReactNativeClient extends BaseClient { this._sendEnvelope(envelope); } - /** - * Sets up the integrations - */ - public setupIntegrations(): void { - super.setupIntegrations(); - } - /** * @inheritDoc */ @@ -123,7 +116,7 @@ export class ReactNativeClient extends BaseClient { } /** - * @inheritdoc + * Sets up the integrations */ protected _setupIntegrations(): void { super._setupIntegrations(); diff --git a/src/js/tracing/nativeframes.ts b/src/js/tracing/nativeframes.ts index 84eab70ec5..9f6e441563 100644 --- a/src/js/tracing/nativeframes.ts +++ b/src/js/tracing/nativeframes.ts @@ -6,6 +6,13 @@ import type { NativeFramesResponse } from '../NativeRNSentry'; import { NATIVE } from '../wrapper'; import { instrumentChildSpanFinish } from './utils'; +/** + * Timeout from the final native frames fetch to processing the associated transaction. + * If the transaction is not processed by this time, the native frames will be dropped + * and not added to the event. + */ +const FINAL_FRAMES_TIMEOUT_MS = 2000; + export interface FramesMeasurements extends Measurements { frames_total: { value: number; unit: MeasurementUnit }; frames_slow: { value: number; unit: MeasurementUnit }; @@ -45,14 +52,24 @@ export class NativeFramesInstrumentation { * Logs the native frames at this start point and instruments child span finishes. */ public onTransactionStart(transaction: Transaction): void { + logger.debug(`[NativeFrames] Fetching frames for root span start (${transaction.spanContext().spanId}).`); void NATIVE.fetchNativeFrames() .then(framesMetrics => { if (framesMetrics) { transaction.setData('__startFrames', framesMetrics); + } else { + logger.warn( + `[NativeFrames] Fetched frames for root span start (${ + transaction.spanContext().spanId + }), but no frames were returned.`, + ); } }) .then(undefined, error => { - logger.error(`[ReactNativeTracing] Error while fetching native frames: ${error}`); + logger.error( + `[NativeFrames] Error while fetching frames for root span start (${transaction.spanContext().spanId})`, + error, + ); }); instrumentChildSpanFinish(transaction, (_: Span, endTimestamp?: number) => { @@ -66,8 +83,11 @@ export class NativeFramesInstrumentation { * To be called when a transaction is finished */ public onTransactionFinish(transaction: Transaction): void { - this._fetchFramesForTransaction(transaction).then(undefined, (reason: unknown) => { - logger.error(`[ReactNativeTracing] Error while fetching native frames:`, reason); + this._fetchEndFramesForTransaction(transaction).then(undefined, (reason: unknown) => { + logger.error( + `[NativeFrames] Error while fetching frames for root span start (${transaction.spanContext().spanId})`, + reason, + ); }); } @@ -88,7 +108,7 @@ export class NativeFramesInstrumentation { } }) .then(undefined, error => { - logger.error(`[ReactNativeTracing] Error while fetching native frames: ${error}`); + logger.error(`[NativeFrames] Error while fetching frames for child span end.`, error); }); } @@ -101,17 +121,20 @@ export class NativeFramesInstrumentation { startFrames: NativeFramesResponse, ): Promise { if (_finishFrames.has(traceId)) { + logger.debug(`[NativeFrames] Native end frames already fetched for trace id (${traceId}).`); return this._prepareMeasurements(traceId, finalEndTimestamp, startFrames); } return new Promise(resolve => { const timeout = setTimeout(() => { + logger.debug(`[NativeFrames] Native end frames listener removed by timeout for trace id (${traceId}).`); _framesListeners.delete(traceId); resolve(null); }, 2000); _framesListeners.set(traceId, () => { + logger.debug(`[NativeFrames] Native end frames listener called for trace id (${traceId}).`); resolve(this._prepareMeasurements(traceId, finalEndTimestamp, startFrames)); clearTimeout(timeout); @@ -137,6 +160,7 @@ export class NativeFramesInstrumentation { // Must be in the margin of error of the actual transaction finish time (finalEndTimestamp) Math.abs(finish.timestamp - finalEndTimestamp) < MARGIN_OF_ERROR_SECONDS ) { + logger.debug(`[NativeFrames] Using frames from root span end (traceId, ${traceId}).`); finalFinishFrames = finish.nativeFrames; } else if ( this._lastSpanFinishFrames && @@ -144,8 +168,12 @@ export class NativeFramesInstrumentation { ) { // Fallback to the last span finish if it is within the margin of error of the actual finish timestamp. // This should be the case for trimEnd. + logger.debug(`[NativeFrames] Using native frames from last span end (traceId, ${traceId}).`); finalFinishFrames = this._lastSpanFinishFrames.nativeFrames; } else { + logger.warn( + `[NativeFrames] Frames were collected within larger than margin of error delay for traceId (${traceId}). Dropping the inaccurate values.`, + ); return null; } @@ -164,13 +192,24 @@ export class NativeFramesInstrumentation { }, }; + if ( + measurements.frames_frozen.value <= 0 && + measurements.frames_slow.value <= 0 && + measurements.frames_total.value <= 0 + ) { + logger.warn( + `[NativeFrames] Detected zero slow or frozen frames. Not adding measurements to traceId (${traceId}).`, + ); + return null; + } + return measurements; } /** * Fetch finish frames for a transaction at the current time. Calls any awaiting listeners. */ - private async _fetchFramesForTransaction(transaction: Transaction): Promise { + private async _fetchEndFramesForTransaction(transaction: Transaction): Promise { const startFrames = transaction.data.__startFrames as NativeFramesResponse | undefined; // This timestamp marks when the finish frames were retrieved. It should be pretty close to the transaction finish. @@ -187,13 +226,13 @@ export class NativeFramesInstrumentation { _framesListeners.get(transaction.traceId)?.(); - setTimeout(() => this._cancelFinishFrames(transaction), 2000); + setTimeout(() => this._cancelEndFrames(transaction), FINAL_FRAMES_TIMEOUT_MS); } /** * On a finish frames failure, we cancel the await. */ - private _cancelFinishFrames(transaction: Transaction): void { + private _cancelEndFrames(transaction: Transaction): void { if (_finishFrames.has(transaction.traceId)) { _finishFrames.delete(transaction.traceId); @@ -222,6 +261,12 @@ export class NativeFramesInstrumentation { const traceId = traceContext.trace_id; + if (!traceContext.data?.__startFrames) { + logger.warn( + `[NativeFrames] Start frames of transaction ${event.transaction} (eventId, ${event.event_id}) are missing, but it already ended.`, + ); + } + if (traceId && traceContext.data?.__startFrames && event.timestamp) { const measurements = await this._getFramesMeasurements( traceId, @@ -229,11 +274,7 @@ export class NativeFramesInstrumentation { traceContext.data.__startFrames as NativeFramesResponse, ); - if (!measurements) { - logger.log( - `[NativeFrames] Could not fetch native frames for ${traceContext.op} transaction ${event.transaction}. Not adding native frames measurements.`, - ); - } else { + if (measurements) { logger.log( `[Measurements] Adding measurements to ${traceContext.op} transaction ${ event.transaction diff --git a/src/js/tracing/reactnativeprofiler.tsx b/src/js/tracing/reactnativeprofiler.tsx index 71fcfb8434..2f5a2f65f1 100644 --- a/src/js/tracing/reactnativeprofiler.tsx +++ b/src/js/tracing/reactnativeprofiler.tsx @@ -1,4 +1,5 @@ -import { getCurrentHub, Profiler } from '@sentry/react'; +import { getClient, getCurrentHub, Profiler } from '@sentry/react'; +import { timestampInSeconds } from '@sentry/utils'; import { createIntegration } from '../integrations/factory'; import { ReactNativeTracing } from './reactnativetracing'; @@ -13,6 +14,13 @@ const ReactNativeProfilerGlobalState = { export class ReactNativeProfiler extends Profiler { public readonly name: string = 'ReactNativeProfiler'; + public constructor(props: ConstructorParameters[0]) { + const client = getClient(); + const integration = client && client.getIntegrationByName && client.getIntegrationByName('ReactNativeTracing'); + integration && integration.setRootComponentFirstConstructorCallTimestampMs(timestampInSeconds() * 1000); + super(props); + } + /** * Get the app root mount time. */ diff --git a/src/js/tracing/reactnativetracing.ts b/src/js/tracing/reactnativetracing.ts index 397cc1ee50..26fad0d053 100644 --- a/src/js/tracing/reactnativetracing.ts +++ b/src/js/tracing/reactnativetracing.ts @@ -7,6 +7,7 @@ import type { Event, EventProcessor, Integration, + Span, Transaction as TransactionType, TransactionContext, } from '@sentry/types'; @@ -23,6 +24,7 @@ import { cancelInBackground, onlySampleIfChildSpans } from './transaction'; import type { BeforeNavigate, RouteChangeContextData } from './types'; import { adjustTransactionDuration, + getBundleStartTimestampMs, getTimeOriginMilliseconds, isNearToNow, setSpanDurationAsMeasurement, @@ -152,6 +154,7 @@ export class ReactNativeTracing implements Integration { private _hasSetTracePropagationTargets: boolean; private _hasSetTracingOrigins: boolean; private _currentViewName: string | undefined; + private _firstConstructorCallTimestampMs: number | undefined; public constructor(options: Partial = {}) { this._hasSetTracePropagationTargets = !!( @@ -276,6 +279,12 @@ export class ReactNativeTracing implements Integration { // Only if this method is called at or within margin of error to the start timestamp. this.nativeFramesInstrumentation?.onTransactionStart(transaction); this.stallTrackingInstrumentation?.onTransactionStart(transaction); + } else { + logger.warn( + `[ReactNativeTracing] onTransactionStart called with delay (larger than margin of error) for transaction ${ + transaction.description + } (${transaction.spanContext().spanId}). Not fetching native frames or tracking stalls.`, + ); } } @@ -294,6 +303,13 @@ export class ReactNativeTracing implements Integration { this._appStartFinishTimestamp = endTimestamp; } + /** + * Sets the root component first constructor call timestamp. + */ + public setRootComponentFirstConstructorCallTimestampMs(timestamp: number): void { + this._firstConstructorCallTimestampMs = timestamp; + } + /** * Starts a new transaction for a user interaction. * @param userInteractionId Consists of `op` representation UI Event and `elementId` unique element identifier on current screen. @@ -404,11 +420,11 @@ export class ReactNativeTracing implements Integration { * Returns the App Start Duration in Milliseconds. Also returns undefined if not able do * define the duration. */ - private _getAppStartDurationMilliseconds(appStart: NativeAppStartResponse): number | undefined { + private _getAppStartDurationMilliseconds(appStartTimestampMs: number): number | undefined { if (!this._appStartFinishTimestamp) { return undefined; } - return this._appStartFinishTimestamp * 1000 - appStart.appStartTime; + return this._appStartFinishTimestamp * 1000 - appStartTimestampMs; } /** @@ -422,11 +438,18 @@ export class ReactNativeTracing implements Integration { const appStart = await NATIVE.fetchNativeAppStart(); - if (!appStart || appStart.didFetchAppStart) { + if (!appStart) { + logger.warn('[ReactNativeTracing] Not instrumenting App Start because native returned null.'); + return; + } + + if (appStart.has_fetched) { + logger.warn('[ReactNativeTracing] Not instrumenting App Start because this start was already reported.'); return; } if (!this.useAppStartWithProfiler) { + logger.warn('[ReactNativeTracing] `Sentry.wrap` not detected, using JS context init as app start end.'); this._appStartFinishTimestamp = getTimeOriginMilliseconds() / 1000; } @@ -448,9 +471,15 @@ export class ReactNativeTracing implements Integration { * Adds app start measurements and starts a child span on a transaction. */ private _addAppStartData(transaction: IdleTransaction, appStart: NativeAppStartResponse): void { - const appStartDurationMilliseconds = this._getAppStartDurationMilliseconds(appStart); + const appStartTimestampMs = appStart.app_start_timestamp_ms; + if (!appStartTimestampMs) { + logger.warn('App start timestamp could not be loaded from the native layer.'); + return; + } + + const appStartDurationMilliseconds = this._getAppStartDurationMilliseconds(appStartTimestampMs); if (!appStartDurationMilliseconds) { - logger.warn('App start was never finished.'); + logger.warn('[ReactNativeTracing] App start end has not been recorded, not adding app start span.'); return; } @@ -458,10 +487,11 @@ export class ReactNativeTracing implements Integration { // this could be due to many different reasons. // we've seen app starts with hours, days and even months. if (appStartDurationMilliseconds >= ReactNativeTracing._maxAppStart) { + logger.warn('[ReactNativeTracing] App start duration is over a minute long, not adding app start span.'); return; } - const appStartTimeSeconds = appStart.appStartTime / 1000; + const appStartTimeSeconds = appStartTimestampMs / 1000; transaction.startTimestamp = appStartTimeSeconds; @@ -477,18 +507,93 @@ export class ReactNativeTracing implements Integration { setSpanDurationAsMeasurement('time_to_full_display', maybeTtfdSpan); } - const op = appStart.isColdStart ? APP_START_COLD_OP : APP_START_WARM_OP; - transaction.startChild({ - description: appStart.isColdStart ? 'Cold App Start' : 'Warm App Start', + const op = appStart.type === 'cold' ? APP_START_COLD_OP : APP_START_WARM_OP; + const appStartSpan = transaction.startChild({ + description: appStart.type === 'cold' ? 'Cold App Start' : 'Warm App Start', op, startTimestamp: appStartTimeSeconds, endTimestamp: this._appStartFinishTimestamp, }); + this._addJSExecutionBeforeRoot(appStartSpan); + this._addNativeSpansTo(appStartSpan, appStart.spans); - const measurement = appStart.isColdStart ? APP_START_COLD : APP_START_WARM; + const measurement = appStart.type === 'cold' ? APP_START_COLD : APP_START_WARM; transaction.setMeasurement(measurement, appStartDurationMilliseconds, 'millisecond'); } + /** + * Adds JS Execution before React Root. If `Sentry.wrap` is not used, create a span for the start of JS Bundle execution. + */ + private _addJSExecutionBeforeRoot(appStartSpan: Span): void { + const bundleStartTimestampMs = getBundleStartTimestampMs(); + if (!bundleStartTimestampMs) { + return; + } + + if (!this._firstConstructorCallTimestampMs) { + logger.warn('Missing the root component first constructor call timestamp.'); + appStartSpan.startChild({ + description: 'JS Bundle Execution Start', + op: appStartSpan.op, + startTimestamp: bundleStartTimestampMs / 1000, + endTimestamp: bundleStartTimestampMs / 1000, + }); + return; + } + + appStartSpan.startChild({ + description: 'JS Bundle Execution Before React Root', + op: appStartSpan.op, + startTimestamp: bundleStartTimestampMs / 1000, + endTimestamp: this._firstConstructorCallTimestampMs / 1000, + }); + } + + /** + * Adds native spans to the app start span. + */ + private _addNativeSpansTo(appStartSpan: Span, nativeSpans: NativeAppStartResponse['spans']): void { + nativeSpans.forEach(span => { + if (span.description === 'UIKit init') { + return this._createUIKitSpan(appStartSpan, span); + } + + appStartSpan.startChild({ + op: appStartSpan.op, + description: span.description, + startTimestamp: span.start_timestamp_ms / 1000, + endTimestamp: span.end_timestamp_ms / 1000, + }); + }); + } + + /** + * UIKit init is measured by the native layers till the native SDK start + * RN initializes the native SDK later, the end timestamp would be wrong + */ + private _createUIKitSpan(parentSpan: Span, nativeUIKitSpan: NativeAppStartResponse['spans'][number]): void { + const bundleStart = getBundleStartTimestampMs(); + + // If UIKit init ends after the bundle start, the native SDK was auto-initialized + // and so the end timestamp is incorrect. + // The timestamps can't equal, as RN initializes after UIKit. + if (bundleStart && bundleStart < nativeUIKitSpan.end_timestamp_ms) { + parentSpan.startChild({ + op: parentSpan.op, + description: 'UIKit Init to JS Exec Start', + startTimestamp: nativeUIKitSpan.start_timestamp_ms / 1000, + endTimestamp: bundleStart / 1000, + }); + } else { + parentSpan.startChild({ + op: parentSpan.op, + description: 'UIKit Init', + startTimestamp: nativeUIKitSpan.start_timestamp_ms / 1000, + endTimestamp: nativeUIKitSpan.end_timestamp_ms / 1000, + }); + } + } + /** To be called when the route changes, but BEFORE the components of the new route mount. */ private _onRouteWillChange(context: TransactionContext): TransactionType | undefined { return this._createRouteTransaction(context); diff --git a/src/js/tracing/timetodisplay.tsx b/src/js/tracing/timetodisplay.tsx index 75fc92bedd..972374d94c 100644 --- a/src/js/tracing/timetodisplay.tsx +++ b/src/js/tracing/timetodisplay.tsx @@ -86,7 +86,10 @@ function TimeToDisplay(props: { * Returns current span if already exists in the currently active span. */ export function startTimeToInitialDisplaySpan( - options?: Exclude & { name?: string; isAutoInstrumented?: boolean }, + options?: Omit & { + name?: string; + isAutoInstrumented?: boolean + }, ): Span | undefined { const activeSpan = getActiveSpan(); if (!activeSpan) { @@ -224,6 +227,7 @@ function updateInitialDisplaySpan(frameTimestampSeconds: number): void { if (fullDisplayBeforeInitialDisplay.has(activeSpan)) { fullDisplayBeforeInitialDisplay.delete(activeSpan); + logger.debug(`[TimeToDisplay] Updating full display with initial display (${span.spanContext().spanId}) end.`); updateFullDisplaySpan(frameTimestampSeconds, span); } @@ -233,7 +237,7 @@ function updateInitialDisplaySpan(frameTimestampSeconds: number): void { function updateFullDisplaySpan(frameTimestampSeconds: number, passedInitialDisplaySpan?: Span): void { const activeSpan = getActiveSpan(); if (!activeSpan) { - logger.warn(`[TimeToDisplay] No active span found to attach ui.load.full_display to.`); + logger.warn(`[TimeToDisplay] No active span found to update ui.load.full_display in.`); return; } @@ -247,25 +251,31 @@ function updateFullDisplaySpan(frameTimestampSeconds: number, passedInitialDispl const initialDisplayEndTimestamp = existingInitialDisplaySpan && spanToJSON(existingInitialDisplaySpan).timestamp; if (!initialDisplayEndTimestamp) { fullDisplayBeforeInitialDisplay.set(activeSpan, true); - logger.warn(`[TimeToDisplay] Full display called before initial display for active span.`); + logger.warn(`[TimeToDisplay] Full display called before initial display for active span (${activeSpan.spanContext().spanId}).`); return; } const span = startTimeToFullDisplaySpan(); if (!span) { - logger.warn(`[TimeToDisplay] No span found or created, possibly performance is disabled.`); + logger.warn(`[TimeToDisplay] No TimeToFullDisplay span found or created, possibly performance is disabled.`); return; } - if (spanToJSON(span).timestamp) { - logger.warn(`[TimeToDisplay] ${spanToJSON(span).description} span already ended.`); + const spanJSON = spanToJSON(span); + if (spanJSON.timestamp) { + logger.warn(`[TimeToDisplay] ${spanJSON.description} (${spanJSON.span_id}) span already ended.`); return; } - span.end(frameTimestampSeconds); + if (initialDisplayEndTimestamp > frameTimestampSeconds) { + logger.warn(`[TimeToDisplay] Using initial display end. Full display end frame timestamp is before initial display end.`); + span.end(initialDisplayEndTimestamp); + } else { + span.end(frameTimestampSeconds); + } span.setStatus('ok'); - logger.debug(`[TimeToDisplay] ${spanToJSON(span).description} span updated with end timestamp.`); + logger.debug(`[TimeToDisplay] ${spanJSON.description} (${spanJSON.span_id}) span updated with end timestamp.`); setSpanDurationAsMeasurement('time_to_full_display', span); } diff --git a/src/js/tracing/utils.ts b/src/js/tracing/utils.ts index de74bfe447..f154322d18 100644 --- a/src/js/tracing/utils.ts +++ b/src/js/tracing/utils.ts @@ -6,7 +6,9 @@ import { spanToJSON, } from '@sentry/core'; import type { Span, TransactionContext, TransactionSource } from '@sentry/types'; -import { timestampInSeconds } from '@sentry/utils'; +import { logger, timestampInSeconds } from '@sentry/utils'; + +import { RN_GLOBAL_OBJ } from '../utils/worldwide'; export const defaultTransactionSource: TransactionSource = 'component'; export const customTransactionSource: TransactionSource = 'custom'; @@ -111,3 +113,25 @@ export function setSpanDurationAsMeasurement(name: string, span: Span): void { setMeasurement(name, (spanEnd - spanStart) * 1000, 'millisecond'); } + +/** + * Returns unix timestamp in ms of the bundle start time. + * + * If not available, returns undefined. + */ +export function getBundleStartTimestampMs(): number | undefined { + const bundleStartTime = RN_GLOBAL_OBJ.__BUNDLE_START_TIME__; + if (!bundleStartTime) { + logger.warn('Missing the bundle start time on the global object.'); + return undefined; + } + + if (!RN_GLOBAL_OBJ.nativePerformanceNow) { + // bundleStartTime is Date.now() in milliseconds + return bundleStartTime; + } + + // nativePerformanceNow() is monotonic clock like performance.now() + const approxStartingTimeOrigin = Date.now() - RN_GLOBAL_OBJ.nativePerformanceNow(); + return approxStartingTimeOrigin + bundleStartTime; +} diff --git a/src/js/utils/worldwide.ts b/src/js/utils/worldwide.ts index 4eedca2992..4f1cfc4c7b 100644 --- a/src/js/utils/worldwide.ts +++ b/src/js/utils/worldwide.ts @@ -22,6 +22,8 @@ export interface ReactNativeInternalGlobal extends InternalGlobal { ___SENTRY_METRO_DEV_SERVER___?: string; }; }; + __BUNDLE_START_TIME__?: number; + nativePerformanceNow?: () => number; } /** Get's the global object for the current JavaScript runtime */ diff --git a/test/client.test.ts b/test/client.test.ts index 2ea3a79710..2cd761f78d 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -667,6 +667,22 @@ describe('Tests ReactNativeClient', () => { expect(client.getIntegrationById('ReactNativeUserInteractionTracing')).toBeTruthy(); }); + test('register user interactions tracing - init()', () => { + const client = new ReactNativeClient( + mockedOptions({ + dsn: EXAMPLE_DSN, + integrations: [ + new ReactNativeTracing({ + enableUserInteractionTracing: true, + }), + ], + }), + ); + client.init(); + + expect(client.getIntegrationById('ReactNativeUserInteractionTracing')).toBeTruthy(); + }); + test('do not register user interactions tracing', () => { const client = new ReactNativeClient( mockedOptions({ @@ -682,6 +698,22 @@ describe('Tests ReactNativeClient', () => { expect(client.getIntegrationById('ReactNativeUserInteractionTracing')).toBeUndefined(); }); + + test('do not register user interactions tracing - init()', () => { + const client = new ReactNativeClient( + mockedOptions({ + dsn: EXAMPLE_DSN, + integrations: [ + new ReactNativeTracing({ + enableUserInteractionTracing: false, + }), + ], + }), + ); + client.init(); + + expect(client.getIntegrationById('ReactNativeUserInteractionTracing')).toBeUndefined(); + }); }); }); diff --git a/test/perf/metrics-ios.yml b/test/perf/metrics-ios.yml index 7b515c263b..1f745c5825 100644 --- a/test/perf/metrics-ios.yml +++ b/test/perf/metrics-ios.yml @@ -10,5 +10,5 @@ startupTimeTest: diffMax: 150 binarySizeTest: - diffMin: 200 KiB - diffMax: 600 KiB + diffMin: 600 KiB + diffMax: 1000 KiB diff --git a/test/tracing/nativeframes.test.ts b/test/tracing/nativeframes.test.ts index 821e92d94e..402fd8a330 100644 --- a/test/tracing/nativeframes.test.ts +++ b/test/tracing/nativeframes.test.ts @@ -98,6 +98,80 @@ describe('NativeFramesInstrumentation', () => { ); }); + it('sets native frames measurements on a transaction event (start frames zero)', async () => { + const startFrames = { + totalFrames: 0, + slowFrames: 0, + frozenFrames: 0, + }; + const finishFrames = { + totalFrames: 100, + slowFrames: 20, + frozenFrames: 5, + }; + mockFunction(NATIVE.fetchNativeFrames).mockResolvedValueOnce(startFrames).mockResolvedValueOnce(finishFrames); + + await startSpan({ name: 'test' }, async () => { + await Promise.resolve(); // native frames fetch is async call this will flush the start frames fetch promise + }); + + await jest.runOnlyPendingTimersAsync(); + await client.flush(); + + expect(client.event!).toEqual( + expect.objectContaining>({ + measurements: expect.objectContaining({ + frames_total: { + value: 100, + unit: 'none', + }, + frames_slow: { + value: 20, + unit: 'none', + }, + frames_frozen: { + value: 5, + unit: 'none', + }, + }), + }), + ); + }); + + it('does not sent zero value native frames measurements', async () => { + const startFrames = { + totalFrames: 100, + slowFrames: 20, + frozenFrames: 5, + }; + const finishFrames = { + totalFrames: 100, + slowFrames: 20, + frozenFrames: 5, + }; + mockFunction(NATIVE.fetchNativeFrames).mockResolvedValueOnce(startFrames).mockResolvedValueOnce(finishFrames); + + await startSpan({ name: 'test' }, async () => { + await Promise.resolve(); // native frames fetch is async call this will flush the start frames fetch promise + }); + + await jest.runOnlyPendingTimersAsync(); + await client.flush(); + + expect(client.event!).toEqual( + expect.objectContaining>({ + measurements: expect.toBeOneOf([ + expect.not.objectContaining({ + frames_total: expect.any(Object), + frames_slow: expect.any(Object), + frames_frozen: expect.any(Object), + }), + undefined, + ]), + }), + ); + }); + it('does not set measurements on transactions without startFrames', async () => { const startFrames = null; const finishFrames = { diff --git a/test/tracing/reactnativetracing.test.ts b/test/tracing/reactnativetracing.test.ts index ff1dfd9b6a..38c9a846ee 100644 --- a/test/tracing/reactnativetracing.test.ts +++ b/test/tracing/reactnativetracing.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as SentryBrowser from '@sentry/browser'; -import type { Event } from '@sentry/types'; +import type { Event, SpanJSON } from '@sentry/types'; import type { NativeAppStartResponse } from '../../src/js/NativeRNSentry'; import { RoutingInstrumentation } from '../../src/js/tracing/routingInstrumentation'; @@ -269,9 +269,10 @@ describe('ReactNativeTracing', () => { const timeOriginMilliseconds = Date.now(); const appStartTimeMilliseconds = timeOriginMilliseconds - 65000; const mockAppStartResponse: NativeAppStartResponse = { - isColdStart: false, - appStartTime: appStartTimeMilliseconds, - didFetchAppStart: false, + type: 'warm', + app_start_timestamp_ms: appStartTimeMilliseconds, + has_fetched: false, + spans: [], }; mockFunction(getTimeOriginMilliseconds).mockReturnValue(timeOriginMilliseconds); @@ -295,9 +296,10 @@ describe('ReactNativeTracing', () => { const timeOriginMilliseconds = Date.now(); const appStartTimeMilliseconds = timeOriginMilliseconds - 65000; const mockAppStartResponse: NativeAppStartResponse = { - isColdStart: false, - appStartTime: appStartTimeMilliseconds, - didFetchAppStart: false, + type: 'warm', + app_start_timestamp_ms: appStartTimeMilliseconds, + has_fetched: false, + spans: [], }; mockFunction(getTimeOriginMilliseconds).mockReturnValue(timeOriginMilliseconds); @@ -315,10 +317,10 @@ describe('ReactNativeTracing', () => { expect(transaction?.start_timestamp).toBeGreaterThanOrEqual(timeOriginMilliseconds / 1000); }); - it('Does not create app start transaction if didFetchAppStart == true', async () => { + it('Does not create app start transaction if has_fetched == true', async () => { const integration = new ReactNativeTracing(); - mockAppStartResponse({ cold: false, didFetchAppStart: true }); + mockAppStartResponse({ cold: false, has_fetched: true }); setup(integration); @@ -328,6 +330,230 @@ describe('ReactNativeTracing', () => { const transaction = client.event; expect(transaction).toBeUndefined(); }); + + describe('bundle execution spans', () => { + afterEach(() => { + clearReactNativeBundleExecutionStartTimestamp(); + }); + + it('does not add bundle executions span if __BUNDLE_START_TIME__ is undefined', async () => { + const integration = new ReactNativeTracing(); + + mockAppStartResponse({ cold: true }); + + setup(integration); + + await jest.advanceTimersByTimeAsync(500); + await jest.runOnlyPendingTimersAsync(); + + const transaction = client.event; + + const bundleStartSpan = transaction!.spans!.find( + ({ description }) => + description === 'JS Bundle Execution Start' || description === 'JS Bundle Execution Before React Root', + ); + + expect(bundleStartSpan).toBeUndefined(); + }); + + it('adds bundle execution span', async () => { + const integration = new ReactNativeTracing(); + + const [timeOriginMilliseconds] = mockAppStartResponse({ cold: true }); + mockReactNativeBundleExecutionStartTimestamp(); + + setup(integration); + integration.onAppStartFinish(timeOriginMilliseconds + 200); + + await jest.advanceTimersByTimeAsync(500); + await jest.runOnlyPendingTimersAsync(); + + const transaction = client.event; + + const appStartRootSpan = transaction!.spans!.find(({ description }) => description === 'Cold App Start'); + const bundleStartSpan = transaction!.spans!.find( + ({ description }) => description === 'JS Bundle Execution Start', + ); + const appStartRootSpanJSON = spanToJSON(appStartRootSpan!); + const bundleStartSpanJSON = spanToJSON(bundleStartSpan!); + + expect(appStartRootSpan).toBeDefined(); + expect(bundleStartSpan).toBeDefined(); + expect(appStartRootSpanJSON).toEqual( + expect.objectContaining({ + description: 'Cold App Start', + span_id: expect.any(String), + op: APP_START_COLD_OP, + }), + ); + expect(bundleStartSpanJSON).toEqual( + expect.objectContaining({ + description: 'JS Bundle Execution Start', + start_timestamp: expect.closeTo((timeOriginMilliseconds - 50) / 1000), + timestamp: expect.closeTo((timeOriginMilliseconds - 50) / 1000), + parent_span_id: spanToJSON(appStartRootSpan!).span_id, // parent is the root app start span + op: spanToJSON(appStartRootSpan!).op, // op is the same as the root app start span + }), + ); + }); + + it('adds bundle execution before react root', async () => { + const integration = new ReactNativeTracing(); + + const [timeOriginMilliseconds] = mockAppStartResponse({ cold: true }); + mockReactNativeBundleExecutionStartTimestamp(); + + setup(integration); + integration.setRootComponentFirstConstructorCallTimestampMs(timeOriginMilliseconds - 10); + + await jest.advanceTimersByTimeAsync(500); + await jest.runOnlyPendingTimersAsync(); + + const transaction = client.event; + + const appStartRootSpan = transaction!.spans!.find(({ description }) => description === 'Cold App Start'); + const bundleStartSpan = transaction!.spans!.find( + ({ description }) => description === 'JS Bundle Execution Before React Root', + ); + const appStartRootSpanJSON = spanToJSON(appStartRootSpan!); + const bundleStartSpanJSON = spanToJSON(bundleStartSpan!); + + expect(appStartRootSpan).toBeDefined(); + expect(bundleStartSpan).toBeDefined(); + expect(appStartRootSpanJSON).toEqual( + expect.objectContaining({ + description: 'Cold App Start', + span_id: expect.any(String), + op: APP_START_COLD_OP, + }), + ); + expect(bundleStartSpanJSON).toEqual( + expect.objectContaining({ + description: 'JS Bundle Execution Before React Root', + start_timestamp: expect.closeTo((timeOriginMilliseconds - 50) / 1000), + timestamp: (timeOriginMilliseconds - 10) / 1000, + parent_span_id: spanToJSON(appStartRootSpan!).span_id, // parent is the root app start span + op: spanToJSON(appStartRootSpan!).op, // op is the same as the root app start span + }), + ); + }); + }); + + it('adds native spans as a child of the main app start span', async () => { + const integration = new ReactNativeTracing(); + + const [timeOriginMilliseconds] = mockAppStartResponse({ + cold: true, + enableNativeSpans: true, + }); + + setup(integration); + + await jest.advanceTimersByTimeAsync(500); + await jest.runOnlyPendingTimersAsync(); + + const transaction = client.event; + + const appStartRootSpan = transaction!.spans!.find(({ description }) => description === 'Cold App Start'); + const nativeSpan = transaction!.spans!.find(({ description }) => description === 'test native app start span'); + const nativeSpanJSON = spanToJSON(nativeSpan!); + const appStartRootSpanJSON = spanToJSON(appStartRootSpan!); + + expect(appStartRootSpan).toBeDefined(); + expect(nativeSpan).toBeDefined(); + expect(appStartRootSpanJSON).toEqual( + expect.objectContaining({ + description: 'Cold App Start', + span_id: expect.any(String), + op: APP_START_COLD_OP, + }), + ); + expect(nativeSpanJSON).toEqual( + expect.objectContaining({ + description: 'test native app start span', + start_timestamp: (timeOriginMilliseconds - 100) / 1000, + timestamp: (timeOriginMilliseconds - 50) / 1000, + parent_span_id: spanToJSON(appStartRootSpan!).span_id, // parent is the root app start span + op: spanToJSON(appStartRootSpan!).op, // op is the same as the root app start span + }), + ); + }); + + it('adds ui kit init full length as a child of the main app start span', async () => { + const integration = new ReactNativeTracing(); + + const timeOriginMilliseconds = Date.now(); + mockAppStartResponse({ + cold: true, + enableNativeSpans: true, + customNativeSpans: [ + { + description: 'UIKit init', + start_timestamp_ms: timeOriginMilliseconds - 100, + end_timestamp_ms: timeOriginMilliseconds - 60, + }, + ], + }); + mockReactNativeBundleExecutionStartTimestamp(); + + setup(integration); + + await jest.advanceTimersByTimeAsync(500); + await jest.runOnlyPendingTimersAsync(); + + const transaction = client.event; + + const nativeSpan = transaction!.spans!.find(({ description }) => description?.startsWith('UIKit Init')); + const nativeSpanJSON = spanToJSON(nativeSpan!); + + expect(nativeSpan).toBeDefined(); + expect(nativeSpanJSON).toEqual( + expect.objectContaining({ + description: 'UIKit Init', + start_timestamp: (timeOriginMilliseconds - 100) / 1000, + timestamp: (timeOriginMilliseconds - 60) / 1000, + }), + ); + }); + + it('adds ui kit init start mark as a child of the main app start span', async () => { + const integration = new ReactNativeTracing(); + + const timeOriginMilliseconds = Date.now(); + mockAppStartResponse({ + cold: true, + enableNativeSpans: true, + customNativeSpans: [ + { + description: 'UIKit init', + start_timestamp_ms: timeOriginMilliseconds - 100, + end_timestamp_ms: timeOriginMilliseconds - 20, // After mocked bundle execution start + }, + ], + }); + mockReactNativeBundleExecutionStartTimestamp(); + + setup(integration); + + await jest.advanceTimersByTimeAsync(500); + await jest.runOnlyPendingTimersAsync(); + + const transaction = client.event; + + const nativeRuntimeInitSpan = transaction!.spans!.find(({ description }) => + description?.startsWith('UIKit Init to JS Exec Start'), + ); + const nativeRuntimeInitSpanJSON = spanToJSON(nativeRuntimeInitSpan!); + + expect(nativeRuntimeInitSpanJSON).toBeDefined(); + expect(nativeRuntimeInitSpanJSON).toEqual( + expect.objectContaining({ + description: 'UIKit Init to JS Exec Start', + start_timestamp: (timeOriginMilliseconds - 100) / 1000, + timestamp: (timeOriginMilliseconds - 50) / 1000, + }), + ); + }); }); describe('With routing instrumentation', () => { @@ -438,14 +664,14 @@ describe('ReactNativeTracing', () => { expect(span!.timestamp).toBe(timeOriginMilliseconds / 1000); }); - it('Does not update route transaction if didFetchAppStart == true', async () => { + it('Does not update route transaction if has_fetched == true', async () => { const routingInstrumentation = new RoutingInstrumentation(); const integration = new ReactNativeTracing({ enableStallTracking: false, routingInstrumentation, }); - const [, appStartTimeMilliseconds] = mockAppStartResponse({ cold: false, didFetchAppStart: true }); + const [, appStartTimeMilliseconds] = mockAppStartResponse({ cold: false, has_fetched: true }); setup(integration); // wait for internal promises to resolve, fetch app start data from mocked native @@ -914,13 +1140,33 @@ describe('ReactNativeTracing', () => { }); }); -function mockAppStartResponse({ cold, didFetchAppStart }: { cold: boolean; didFetchAppStart?: boolean }) { +function mockAppStartResponse({ + cold, + has_fetched, + enableNativeSpans, + customNativeSpans, +}: { + cold: boolean; + has_fetched?: boolean; + enableNativeSpans?: boolean; + customNativeSpans?: NativeAppStartResponse['spans']; +}) { const timeOriginMilliseconds = Date.now(); const appStartTimeMilliseconds = timeOriginMilliseconds - 100; const mockAppStartResponse: NativeAppStartResponse = { - isColdStart: cold, - appStartTime: appStartTimeMilliseconds, - didFetchAppStart: didFetchAppStart ?? false, + type: cold ? 'cold' : 'warm', + app_start_timestamp_ms: appStartTimeMilliseconds, + has_fetched: has_fetched ?? false, + spans: enableNativeSpans + ? [ + { + description: 'test native app start span', + start_timestamp_ms: timeOriginMilliseconds - 100, + end_timestamp_ms: timeOriginMilliseconds - 50, + }, + ...(customNativeSpans ?? []), + ] + : [], }; mockFunction(getTimeOriginMilliseconds).mockReturnValue(timeOriginMilliseconds); @@ -932,3 +1178,20 @@ function mockAppStartResponse({ cold, didFetchAppStart }: { cold: boolean; didFe function setup(integration: ReactNativeTracing) { integration.setupOnce(addGlobalEventProcessor, getCurrentHub); } + +/** + * Mocks RN Bundle Start Module + * `var __BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now()` + */ +function mockReactNativeBundleExecutionStartTimestamp() { + RN_GLOBAL_OBJ.nativePerformanceNow = () => 100; // monotonic clock like `performance.now()` + RN_GLOBAL_OBJ.__BUNDLE_START_TIME__ = 50; // 50ms after time origin +} + +/** + * Removes mock added by mockReactNativeBundleExecutionStartTimestamp + */ +function clearReactNativeBundleExecutionStartTimestamp() { + delete RN_GLOBAL_OBJ.nativePerformanceNow; + delete RN_GLOBAL_OBJ.__BUNDLE_START_TIME__; +} diff --git a/test/tracing/reactnavigation.ttid.test.tsx b/test/tracing/reactnavigation.ttid.test.tsx index 200257957f..df840a3e13 100644 --- a/test/tracing/reactnavigation.ttid.test.tsx +++ b/test/tracing/reactnavigation.ttid.test.tsx @@ -36,9 +36,10 @@ describe('React Navigation - TTID', () => { (isHermesEnabled as jest.Mock).mockReturnValue(true); mockWrapper.NATIVE.fetchNativeAppStart.mockResolvedValue({ - appStartTime: mockedAppStartTimeSeconds * 1000, - didFetchAppStart: false, - isColdStart: true, + app_start_timestamp_ms: mockedAppStartTimeSeconds * 1000, + has_fetched: false, + type: 'cold', + spans: [], }); mockedEventEmitter = mockedSentryEventEmitter.createMockedSentryEventEmitter(); diff --git a/test/tracing/timetodisplay.test.tsx b/test/tracing/timetodisplay.test.tsx index 42db2c115b..9abe22bed4 100644 --- a/test/tracing/timetodisplay.test.tsx +++ b/test/tracing/timetodisplay.test.tsx @@ -241,6 +241,42 @@ describe('TimeToDisplay', () => { expect(spanToJSON(fullDisplaySpan!).timestamp).toEqual(initialDisplayEndTimestampMs / 1_000); }); + test('full display which ended before but processed after initial display is extended to initial display end', async () => { + const fullDisplayEndTimestampMs = secondInFutureTimestampMs(); + const initialDisplayEndTimestampMs = secondInFutureTimestampMs() + 500; + const [initialDisplaySpan, fullDisplaySpan, activeSpan] = startSpanManual( + { + name: 'Root Manual Span', + startTime: secondAgoTimestampMs(), + }, + (activeSpan: Span | undefined) => { + const initialDisplaySpan = startTimeToInitialDisplaySpan(); + const fullDisplaySpan = startTimeToFullDisplaySpan(); + + const timeToDisplayComponent = TestRenderer.create(<>); + timeToDisplayComponent.update(<>); + + emitNativeInitialDisplayEvent(initialDisplayEndTimestampMs); + emitNativeFullDisplayEvent(fullDisplayEndTimestampMs); + + activeSpan?.end(); + return [initialDisplaySpan, fullDisplaySpan, activeSpan]; + }, + ); + + await jest.runOnlyPendingTimersAsync(); + await client.flush(); + + expectFinishedInitialDisplaySpan(initialDisplaySpan, activeSpan); + expectFinishedFullDisplaySpan(fullDisplaySpan, activeSpan); + + expectInitialDisplayMeasurementOnSpan(client.event!); + expectFullDisplayMeasurementOnSpan(client.event!); + + expect(spanToJSON(initialDisplaySpan!).timestamp).toEqual(initialDisplayEndTimestampMs / 1_000); + expect(spanToJSON(fullDisplaySpan!).timestamp).toEqual(initialDisplayEndTimestampMs / 1_000); + }); + test('consequent renders do not update display end', async () => { const initialDisplayEndTimestampMs = secondInFutureTimestampMs(); const fullDisplayEndTimestampMs = secondInFutureTimestampMs() + 500; diff --git a/yarn.lock b/yarn.lock index 02c15e9e27..d36f2dd66b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3739,13 +3739,13 @@ component-type "^1.2.1" join-component "^1.1.0" -"@sentry-internal/eslint-config-sdk@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/eslint-config-sdk/-/eslint-config-sdk-7.113.0.tgz#37a86b7bdf71cfab47d1108d27306f763bc37862" - integrity sha512-VaIVKbSymUq4FjehYZe+l/VhyD+KDf32HCL/7zdENbZXlgH+SO/oS4Iq1T2hc/W54D3rC1V8+YViaKQEbVmhcg== +"@sentry-internal/eslint-config-sdk@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/eslint-config-sdk/-/eslint-config-sdk-7.117.0.tgz#27e9923a727d6ca56060047aab7af60a9d0360f4" + integrity sha512-LBj5oYTbdqCulebIZVeAj4LPs0UhFK2ZrBXyUgtKchZWahTk1L7eioXd2fmtIuJ9pFmvczS2UMJ0oRdtKy08Mg== dependencies: - "@sentry-internal/eslint-plugin-sdk" "7.113.0" - "@sentry-internal/typescript" "7.113.0" + "@sentry-internal/eslint-plugin-sdk" "7.117.0" + "@sentry-internal/typescript" "7.117.0" "@typescript-eslint/eslint-plugin" "^5.48.0" "@typescript-eslint/parser" "^5.48.0" eslint-config-prettier "^6.11.0" @@ -3755,40 +3755,40 @@ eslint-plugin-jsdoc "^30.0.3" eslint-plugin-simple-import-sort "^5.0.3" -"@sentry-internal/eslint-plugin-sdk@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/eslint-plugin-sdk/-/eslint-plugin-sdk-7.113.0.tgz#cafb9b2bc8560c9baf8ffe05eb93703229492e5b" - integrity sha512-c4EGfRX4BECKB9EB9eS1oOvnkPXXRe4i9N3AlVHJrbamoS0Qqrxx1PRDvl3Gd8iI5NEw+1gAlLc2NgR9qRJ2bw== +"@sentry-internal/eslint-plugin-sdk@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/eslint-plugin-sdk/-/eslint-plugin-sdk-7.117.0.tgz#110aae111143a13f8fdb196b09a1e721bc964dad" + integrity sha512-M6IuQCc+DPP4YqBS8HQI4B5B8+iB0Dwb3YvTO7mIfDOCmRIM//7j8Mn1rvOBpTeb3qwJnYsmlHg15jiSgtiLzQ== dependencies: requireindex "~1.1.0" -"@sentry-internal/feedback@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.113.0.tgz#90a3c5493e289d589cfde79330fca549a24f41a4" - integrity sha512-eEmL8QXauUnM3FXGv0GT29RpL0Jo0pkn/uMu3aqjhQo7JKNqUGVYIUxJxiGWbVMbDXqPQ7L66bjjMS3FR1GM2g== +"@sentry-internal/feedback@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.117.0.tgz#4ca62cc469611720e76877a756cf24b792cb178e" + integrity sha512-4X+NnnY17W74TymgLFH7/KPTVYpEtoMMJh8HzVdCmHTOE6j32XKBeBMRaXBhmNYmEgovgyRKKf2KvtSfgw+V1Q== dependencies: - "@sentry/core" "7.113.0" - "@sentry/types" "7.113.0" - "@sentry/utils" "7.113.0" + "@sentry/core" "7.117.0" + "@sentry/types" "7.117.0" + "@sentry/utils" "7.117.0" -"@sentry-internal/replay-canvas@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.113.0.tgz#8a0165494b0a0ba7b1ae45166ca90a8749c38b7a" - integrity sha512-K8uA42aobNF/BAXf14el15iSAi9fonLBUrjZi6nPDq7zaA8rPvfcTL797hwCbqkETz2zDf52Jz7I3WFCshDoUw== +"@sentry-internal/replay-canvas@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.117.0.tgz#d6b3b711453c88e040f31ebab1d4bc627b4a6505" + integrity sha512-7hjIhwEcoosr+BIa0AyEssB5xwvvlzUpvD5fXu4scd3I3qfX8gdnofO96a8r+LrQm3bSj+eN+4TfKEtWb7bU5A== dependencies: - "@sentry/core" "7.113.0" - "@sentry/replay" "7.113.0" - "@sentry/types" "7.113.0" - "@sentry/utils" "7.113.0" + "@sentry/core" "7.117.0" + "@sentry/replay" "7.117.0" + "@sentry/types" "7.117.0" + "@sentry/utils" "7.117.0" -"@sentry-internal/tracing@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.113.0.tgz#936f23205ab53be62f1753b923eddc243cefde86" - integrity sha512-8MDnYENRMnEfQjvN4gkFYFaaBSiMFSU/6SQZfY9pLI3V105z6JQ4D0PGMAUVowXilwNZVpKNYohE7XByuhEC7Q== +"@sentry-internal/tracing@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.117.0.tgz#c7d2357dae8d7ea2bc130e4513ac4ffc8dc7553c" + integrity sha512-fAIyijNvKBZNA12IcKo+dOYDRTNrzNsdzbm3DP37vJRKVQu19ucqP4Y6InvKokffDP2HZPzFPDoGXYuXkDhUZg== dependencies: - "@sentry/core" "7.113.0" - "@sentry/types" "7.113.0" - "@sentry/utils" "7.113.0" + "@sentry/core" "7.117.0" + "@sentry/types" "7.117.0" + "@sentry/utils" "7.117.0" "@sentry-internal/tracing@7.76.0": version "7.76.0" @@ -3799,24 +3799,24 @@ "@sentry/types" "7.76.0" "@sentry/utils" "7.76.0" -"@sentry-internal/typescript@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/typescript/-/typescript-7.113.0.tgz#beb089d537f5267578e81d5dca47f0cf7fdb5875" - integrity sha512-zUjWxuBzY/ROXyeU487xvTq88BMDi9HRgKJ/XBgkse+tR+gtDTygPdToxNEVEMceLaPsHxi817/cAXIEJ5zyXQ== - -"@sentry/browser@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.113.0.tgz#09b77812cbf476eacdccdc714ba4e4ba2c170a88" - integrity sha512-PdyVHPOprwoxGfKGsP2dXDWO0MBDW1eyP7EZlfZvM1A4hjk6ZRNfCv30g+TrqX4hiZDKzyqN3+AdP7N/J2IX0Q== - dependencies: - "@sentry-internal/feedback" "7.113.0" - "@sentry-internal/replay-canvas" "7.113.0" - "@sentry-internal/tracing" "7.113.0" - "@sentry/core" "7.113.0" - "@sentry/integrations" "7.113.0" - "@sentry/replay" "7.113.0" - "@sentry/types" "7.113.0" - "@sentry/utils" "7.113.0" +"@sentry-internal/typescript@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/typescript/-/typescript-7.117.0.tgz#bd43fc07a222e98861e6ab8a85ddd60e7399cd47" + integrity sha512-SylReCEo1FiTuir6XiZuV+sWBOBERDL0C3YmdHhczOh0aeu50FUja7uJfoXMx0LTEwaUAXq62dWUvb9WetluOQ== + +"@sentry/browser@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.117.0.tgz#3030073f360974dadcf5a5f2e1542497b3be2482" + integrity sha512-29X9HlvDEKIaWp6XKlNPPSNND0U6P/ede5WA2nVHfs1zJLWdZ7/ijuMc0sH/CueEkqHe/7gt94hBcI7HOU/wSw== + dependencies: + "@sentry-internal/feedback" "7.117.0" + "@sentry-internal/replay-canvas" "7.117.0" + "@sentry-internal/tracing" "7.117.0" + "@sentry/core" "7.117.0" + "@sentry/integrations" "7.117.0" + "@sentry/replay" "7.117.0" + "@sentry/types" "7.117.0" + "@sentry/utils" "7.117.0" "@sentry/cli-darwin@2.31.2": version "2.31.2" @@ -3885,13 +3885,13 @@ proxy-from-env "^1.1.0" which "^2.0.2" -"@sentry/core@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.113.0.tgz#84307eabf03ece9304894ad24ee15581a220c5c7" - integrity sha512-pg75y3C5PG2+ur27A0Re37YTCEnX0liiEU7EOxWDGutH17x3ySwlYqLQmZsFZTSnvzv7t3MGsNZ8nT5O0746YA== +"@sentry/core@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.117.0.tgz#eebdb6e700d5fbdf3102c4abfb4ff92ef79ae9a5" + integrity sha512-1XZ4/d/DEwnfM2zBMloXDwX+W7s76lGKQMgd8bwgPJZjjEztMJ7X0uopKAGwlQcjn242q+hsCBR6C+fSuI5kvg== dependencies: - "@sentry/types" "7.113.0" - "@sentry/utils" "7.113.0" + "@sentry/types" "7.117.0" + "@sentry/utils" "7.117.0" "@sentry/core@7.76.0": version "7.76.0" @@ -3901,23 +3901,23 @@ "@sentry/types" "7.76.0" "@sentry/utils" "7.76.0" -"@sentry/hub@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.113.0.tgz#12f14071f43e657cd36174ba8b06cc955da5492f" - integrity sha512-aoerhlAw3vnY9a27eKAoK862oMXFbyMFWbaZuCeR5gfg7sHsOkVQkCl3yiYfF5hfw9MbwbbY6GqWbCrA89Ci/A== +"@sentry/hub@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.117.0.tgz#924462cd083b57b45559eb5a25850e5b3004a7f8" + integrity sha512-pQrXnbzsRHCUsVIqz/sZ0vggnxuuHqsmyjoy2Ha1qn1Ya4QbyAWEEGoZTnZx6I/Vt3dzVvRnR3YCywatdkaFxg== dependencies: - "@sentry/core" "7.113.0" - "@sentry/types" "7.113.0" - "@sentry/utils" "7.113.0" + "@sentry/core" "7.117.0" + "@sentry/types" "7.117.0" + "@sentry/utils" "7.117.0" -"@sentry/integrations@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.113.0.tgz#cce71e07cf90c4bf9b22f85c3ce22d9ba926ae5a" - integrity sha512-w0sspGBQ+6+V/9bgCkpuM3CGwTYoQEVeTW6iNebFKbtN7MrM3XsGAM9I2cW1jVxFZROqCBPFtd2cs5n0j14aAg== +"@sentry/integrations@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.117.0.tgz#4613dae3bc1d257c3c870461327fd4f70dbda229" + integrity sha512-U3suSZysmU9EiQqg0ga5CxveAyNbi9IVdsapMDq5EQGNcVDvheXtULs+BOc11WYP3Kw2yWB38VDqLepfc/Fg2g== dependencies: - "@sentry/core" "7.113.0" - "@sentry/types" "7.113.0" - "@sentry/utils" "7.113.0" + "@sentry/core" "7.117.0" + "@sentry/types" "7.117.0" + "@sentry/utils" "7.117.0" localforage "^1.8.1" "@sentry/node@^7.69.0": @@ -3931,43 +3931,43 @@ "@sentry/utils" "7.76.0" https-proxy-agent "^5.0.0" -"@sentry/react@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.113.0.tgz#8e21c92e9691ea881639596d7e60a996b23ba229" - integrity sha512-+zVPz+h5Wydq4ntekw3/dXq5jeHIpZoQ2iqhB96PA9Y94JIq178i/xIP204S1h6rN7cmWAqtR93vnPKdxnlUbQ== +"@sentry/react@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.117.0.tgz#0a6e729f5d17782a02a48728821536ede569bc8d" + integrity sha512-aK+yaEP2esBhaczGU96Y7wkqB4umSIlRAzobv7ER88EGHzZulRaocTpQO8HJJGDHm4D8rD+E893BHnghkoqp4Q== dependencies: - "@sentry/browser" "7.113.0" - "@sentry/core" "7.113.0" - "@sentry/types" "7.113.0" - "@sentry/utils" "7.113.0" + "@sentry/browser" "7.117.0" + "@sentry/core" "7.117.0" + "@sentry/types" "7.117.0" + "@sentry/utils" "7.117.0" hoist-non-react-statics "^3.3.2" -"@sentry/replay@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.113.0.tgz#db41b792e5d9966a9b1ca4eb1695ad7100f39b50" - integrity sha512-UD2IaphOWKFdeGR+ZiaNAQ+wFsnwbJK6PNwcW6cHmWKv9COlKufpFt06lviaqFZ8jmNrM4H+r+R8YVTrqCuxgg== +"@sentry/replay@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.117.0.tgz#c41844b60ad5d711d663a562e2df77fe14c51bbb" + integrity sha512-V4DfU+x4UsA4BsufbQ8jHYa5H0q5PYUgso2X1PR31g1fpx7yiYguSmCfz1UryM6KkH92dfTnqXapDB44kXOqzQ== dependencies: - "@sentry-internal/tracing" "7.113.0" - "@sentry/core" "7.113.0" - "@sentry/types" "7.113.0" - "@sentry/utils" "7.113.0" + "@sentry-internal/tracing" "7.117.0" + "@sentry/core" "7.117.0" + "@sentry/types" "7.117.0" + "@sentry/utils" "7.117.0" -"@sentry/types@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.113.0.tgz#2193c9933838302c82814771b03a8647fa684ffb" - integrity sha512-PJbTbvkcPu/LuRwwXB1He8m+GjDDLKBtu3lWg5xOZaF5IRdXQU2xwtdXXsjge4PZR00tF7MO7X8ZynTgWbYaew== +"@sentry/types@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.117.0.tgz#c4d89aba487c05f4e5cbfa2f1c65180b536393b4" + integrity sha512-5dtdulcUttc3F0Te7ekZmpSp/ebt/CA71ELx0uyqVGjWsSAINwskFD77sdcjqvZWek//WjiYX1+GRKlpJ1QqsA== "@sentry/types@7.76.0": version "7.76.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.76.0.tgz#628c9899bfa82ea762708314c50fd82f2138587d" integrity sha512-vj6z+EAbVrKAXmJPxSv/clpwS9QjPqzkraMFk2hIdE/kii8s8kwnkBwTSpIrNc8GnzV3qYC4r3qD+BXDxAGPaw== -"@sentry/utils@7.113.0": - version "7.113.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.113.0.tgz#1e6e790c9d84e4809b2bb529bbd33a506b6db7bd" - integrity sha512-nzKsErwmze1mmEsbW2AwL2oB+I5v6cDEJY4sdfLekA4qZbYZ8pV5iWza6IRl4XfzGTE1qpkZmEjPU9eyo0yvYw== +"@sentry/utils@7.117.0": + version "7.117.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.117.0.tgz#ac367a6f623bd09440b39d947437009c0ffe52b2" + integrity sha512-KkcLY8643SGBiDyPvMQOubBkwVX5IPknMHInc7jYC8pDVncGp7C65Wi506bCNPpKCWspUd/0VDNWOOen51/qKA== dependencies: - "@sentry/types" "7.113.0" + "@sentry/types" "7.117.0" "@sentry/utils@7.76.0": version "7.76.0" @@ -12955,7 +12955,7 @@ string-length@^5.0.1: char-regex "^2.0.0" strip-ansi "^7.0.1" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12973,15 +12973,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -13133,7 +13124,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13168,13 +13159,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -14170,7 +14154,7 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -14188,15 +14172,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"