diff --git a/CHANGELOG.md b/CHANGELOG.md index f53e242939..e6d4c84694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased](https://github.com/Instabug/Instabug-React-Native/compare/v14.0.0...dev) + +### Added + +- Add support for tracing network requests from Instabug to services like Datadog and New Relic ([#1288](https://github.com/Instabug/Instabug-React-Native/pull/1288)) + ## [14.0.0](https://github.com/Instabug/Instabug-React-Native/compare/v13.4.0...14.0.0) (November 19, 2024) ### Added diff --git a/android/src/main/java/com/instabug/reactlibrary/Constants.java b/android/src/main/java/com/instabug/reactlibrary/Constants.java index fcab683326..f6986200d3 100644 --- a/android/src/main/java/com/instabug/reactlibrary/Constants.java +++ b/android/src/main/java/com/instabug/reactlibrary/Constants.java @@ -9,6 +9,9 @@ final class Constants { final static String IBG_ON_NEW_MESSAGE_HANDLER = "IBGonNewMessageHandler"; final static String IBG_ON_NEW_REPLY_RECEIVED_CALLBACK = "IBGOnNewReplyReceivedCallback"; + + final static String IBG_ON_NEW_W3C_FLAGS_UPDATE_RECEIVED_CALLBACK = "IBGOnNewW3CFlagsUpdateReceivedCallback"; + final static String IBG_SESSION_REPLAY_ON_SYNC_CALLBACK_INVOCATION = "IBGSessionReplayOnSyncCallback"; } diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java index e2ff10b991..8b3c3206eb 100644 --- a/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java +++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugAPMModule.java @@ -9,17 +9,16 @@ import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; import com.instabug.apm.APM; import com.instabug.apm.model.ExecutionTrace; import com.instabug.apm.networking.APMNetworkLogger; import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; +import com.instabug.reactlibrary.utils.EventEmitterModule; +import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; import com.instabug.reactlibrary.utils.MainThreadHandler; -import org.json.JSONException; -import org.json.JSONObject; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; @@ -28,7 +27,7 @@ import static com.instabug.reactlibrary.utils.InstabugUtil.getMethod; -public class RNInstabugAPMModule extends ReactContextBaseJavaModule { +public class RNInstabugAPMModule extends EventEmitterModule { public RNInstabugAPMModule(ReactApplicationContext reactApplicationContext) { super(reactApplicationContext); @@ -330,14 +329,41 @@ private void networkLogAndroid(final double requestStartTime, final double statusCode, final String responseContentType, @Nullable final String errorDomain, + @Nullable final ReadableMap w3cAttributes, @Nullable final String gqlQueryName, - @Nullable final String serverErrorMessage) { + @Nullable final String serverErrorMessage + ) { try { APMNetworkLogger networkLogger = new APMNetworkLogger(); final boolean hasError = errorDomain != null && !errorDomain.isEmpty(); final String errorMessage = hasError ? errorDomain : null; + Boolean isW3cHeaderFound=false; + Long partialId=null; + Long networkStartTimeInSeconds=null; + + + try { + if (!w3cAttributes.isNull("isW3cHeaderFound")) { + isW3cHeaderFound = w3cAttributes.getBoolean("isW3cHeaderFound"); + } + if (!w3cAttributes.isNull("partialId")) { + partialId =(long) w3cAttributes.getDouble("partialId"); + networkStartTimeInSeconds = (long) w3cAttributes.getDouble("networkStartTimeInSeconds"); + } + + } catch (Exception e) { + e.printStackTrace(); + } + APMCPNetworkLog.W3CExternalTraceAttributes w3cExternalTraceAttributes = + new APMCPNetworkLog.W3CExternalTraceAttributes( + isW3cHeaderFound, + partialId, + networkStartTimeInSeconds, + w3cAttributes.getString("w3cGeneratedHeader"), + w3cAttributes.getString("w3cCaughtHeader") + ); try { Method method = getMethod(Class.forName("com.instabug.apm.networking.APMNetworkLogger"), "log", long.class, long.class, String.class, String.class, long.class, String.class, String.class, String.class, String.class, String.class, long.class, int.class, String.class, String.class, String.class, String.class, APMCPNetworkLog.W3CExternalTraceAttributes.class); if (method != null) { @@ -359,7 +385,7 @@ private void networkLogAndroid(final double requestStartTime, errorMessage, gqlQueryName, serverErrorMessage, - null + w3cExternalTraceAttributes ); } else { Log.e("IB-CP-Bridge", "APMNetworkLogger.log was not found by reflection"); diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java index f936eaa12e..9c901cb7a5 100644 --- a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java +++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java @@ -8,6 +8,7 @@ import android.util.Log; import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.UiThread; import com.facebook.react.bridge.Arguments; @@ -22,6 +23,8 @@ import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.uimanager.UIManagerModule; +import com.instabug.apm.InternalAPM; +import com.instabug.apm.configuration.cp.APMFeature; import com.instabug.library.Feature; import com.instabug.library.Instabug; import com.instabug.library.InstabugColorTheme; @@ -30,6 +33,11 @@ import com.instabug.library.LogLevel; import com.instabug.library.ReproConfigurations; import com.instabug.library.core.InstabugCore; +import com.instabug.library.internal.crossplatform.CoreFeature; +import com.instabug.library.internal.crossplatform.CoreFeaturesState; +import com.instabug.library.internal.crossplatform.FeaturesStateListener; +import com.instabug.library.internal.crossplatform.InternalCore; +import com.instabug.library.featuresflags.model.IBGFeatureFlag; import com.instabug.library.featuresflags.model.IBGFeatureFlag; import com.instabug.library.internal.module.InstabugLocale; import com.instabug.library.invocation.InstabugInvocationEvent; @@ -1148,6 +1156,105 @@ public void run() { } }); } + /** + * Register a listener for W3C flags value change + */ + @ReactMethod + public void registerW3CFlagsChangeListener(){ + + MainThreadHandler.runOnMainThread(new Runnable() { + @Override + public void run() { + try { + InternalCore.INSTANCE._setFeaturesStateListener(new FeaturesStateListener() { + @Override + public void invoke(@NonNull CoreFeaturesState featuresState) { + WritableMap params = Arguments.createMap(); + params.putBoolean("isW3ExternalTraceIDEnabled", featuresState.isW3CExternalTraceIdEnabled()); + params.putBoolean("isW3ExternalGeneratedHeaderEnabled", featuresState.isAttachingGeneratedHeaderEnabled()); + params.putBoolean("isW3CaughtHeaderEnabled", featuresState.isAttachingCapturedHeaderEnabled()); + + sendEvent(Constants.IBG_ON_NEW_W3C_FLAGS_UPDATE_RECEIVED_CALLBACK, params); + } + }); + } + catch (Exception e) { + e.printStackTrace(); + } + + } + + }); + } + + + /** + * Get first time Value of W3ExternalTraceID flag + */ + @ReactMethod + public void isW3ExternalTraceIDEnabled(Promise promise){ + + MainThreadHandler.runOnMainThread(new Runnable() { + @Override + public void run() { + try { + promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_EXTERNAL_TRACE_ID)); + } + catch (Exception e) { + e.printStackTrace(); + promise.resolve(false); + } + + } + + }); + } + + + /** + * Get first time Value of W3ExternalGeneratedHeader flag + */ + @ReactMethod + public void isW3ExternalGeneratedHeaderEnabled(Promise promise){ + + MainThreadHandler.runOnMainThread(new Runnable() { + @Override + public void run() { + try { + promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_GENERATED_HEADER)); + } + catch (Exception e) { + e.printStackTrace(); + promise.resolve(false); + } + + } + + }); + } + + /** + * Get first time Value of W3CaughtHeader flag + */ + @ReactMethod + public void isW3CaughtHeaderEnabled(Promise promise){ + + MainThreadHandler.runOnMainThread(new Runnable() { + @Override + public void run() { + try { + promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_CAPTURED_HEADER)); + } + catch (Exception e) { + e.printStackTrace(); + promise.resolve(false); + } + + } + + }); + } + /** * Map between the exported JS constant and the arg key in {@link ArgsRegistry}. diff --git a/android/src/test/java/com/instabug/reactlibrary/RNInstabugAPMModuleTest.java b/android/src/test/java/com/instabug/reactlibrary/RNInstabugAPMModuleTest.java index 5045f929e6..85ca1384d1 100644 --- a/android/src/test/java/com/instabug/reactlibrary/RNInstabugAPMModuleTest.java +++ b/android/src/test/java/com/instabug/reactlibrary/RNInstabugAPMModuleTest.java @@ -204,4 +204,6 @@ public void testSetFlowAttribute() { verify(APM.class, times(1)); APM.endUITrace(); } + + } diff --git a/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java b/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java index b9bf2308cc..e8aad5b0c5 100644 --- a/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java +++ b/android/src/test/java/com/instabug/reactlibrary/RNInstabugReactnativeModuleTest.java @@ -18,6 +18,9 @@ import com.instabug.library.IssueType; import com.instabug.library.ReproConfigurations; import com.instabug.library.ReproMode; +import com.instabug.library.internal.crossplatform.CoreFeature; +import com.instabug.library.internal.crossplatform.InternalCore; +import com.instabug.library.featuresflags.model.IBGFeatureFlag; import com.instabug.library.featuresflags.model.IBGFeatureFlag; import com.instabug.library.internal.module.InstabugLocale; import com.instabug.library.ui.onboarding.WelcomeMessage; @@ -635,4 +638,28 @@ public void testWillRedirectToStore() { // then mockInstabug.verify(() -> Instabug.willRedirectToStore()); } + @Test + public void testW3CExternalTraceIDFlag(){ + Promise promise = mock(Promise.class); + InternalCore internalAPM = mock(InternalCore.class); + rnModule.isW3ExternalTraceIDEnabled(promise); + boolean expected=internalAPM._isFeatureEnabled(CoreFeature.W3C_EXTERNAL_TRACE_ID); + verify(promise).resolve(expected); + } + @Test + public void testW3CExternalGeneratedHeaderFlag(){ + Promise promise = mock(Promise.class); + InternalCore internalAPM = mock(InternalCore.class); + rnModule.isW3ExternalGeneratedHeaderEnabled(promise); + boolean expected=internalAPM._isFeatureEnabled(CoreFeature.W3C_ATTACHING_GENERATED_HEADER); + verify(promise).resolve(expected); + } + @Test + public void testW3CCaughtHeaderFlag(){ + Promise promise = mock(Promise.class); + InternalCore internalAPM = mock(InternalCore.class); + rnModule.isW3CaughtHeaderEnabled(promise); + boolean expected=internalAPM._isFeatureEnabled(CoreFeature.W3C_ATTACHING_CAPTURED_HEADER); + verify(promise).resolve(expected); + } } diff --git a/examples/default/ios/InstabugTests/InstabugAPMTests.m b/examples/default/ios/InstabugTests/InstabugAPMTests.m index dd96841dea..5945b2b791 100644 --- a/examples/default/ios/InstabugTests/InstabugAPMTests.m +++ b/examples/default/ios/InstabugTests/InstabugAPMTests.m @@ -13,6 +13,7 @@ #import #import "Instabug/Instabug.h" #import "IBGConstants.h" +#import "RNInstabug/IBGAPM+PrivateAPIs.h" @interface InstabugAPMTests : XCTestCase @property (nonatomic, retain) InstabugAPMBridge *instabugBridge; @@ -176,4 +177,6 @@ - (void) testEndUITrace { OCMVerify([mock endUITrace]); } + + @end diff --git a/examples/default/ios/InstabugTests/InstabugCrashReportingTests.m b/examples/default/ios/InstabugTests/InstabugCrashReportingTests.m index d42e47022a..d8ae7a0e54 100644 --- a/examples/default/ios/InstabugTests/InstabugCrashReportingTests.m +++ b/examples/default/ios/InstabugTests/InstabugCrashReportingTests.m @@ -19,11 +19,13 @@ - (void)setUp { } - (void)testSetEnabled { + + [self.bridge setEnabled:NO]; + XCTAssertFalse(IBGCrashReporting.enabled); + [self.bridge setEnabled:YES]; XCTAssertTrue(IBGCrashReporting.enabled); - [self.bridge setEnabled:NO]; - XCTAssertFalse(IBGCrashReporting.enabled); } - (void)testSendJSCrash { diff --git a/examples/default/ios/InstabugTests/InstabugSampleTests.m b/examples/default/ios/InstabugTests/InstabugSampleTests.m index 51bbe182c2..8744ce4eb8 100644 --- a/examples/default/ios/InstabugTests/InstabugSampleTests.m +++ b/examples/default/ios/InstabugTests/InstabugSampleTests.m @@ -315,7 +315,7 @@ - (void)testSetWelcomeMessageMode { - (void)testNetworkLogIOS { id mIBGNetworkLogger = OCMClassMock([IBGNetworkLogger class]); - + NSString *url = @"https://api.instabug.com"; NSString *method = @"GET"; NSString *requestBody = @"requestBody"; @@ -332,7 +332,12 @@ - (void)testNetworkLogIOS { double duration = 150; NSString *gqlQueryName = nil; NSString *serverErrorMessage = nil; - + NSDictionary* w3cExternalTraceAttributes = nil; + NSNumber *isW3cCaughted = nil; + NSNumber *partialID = nil; + NSNumber *timestamp= nil; + NSString *generatedW3CTraceparent= nil; + NSString *caughtedW3CTraceparent= nil; [self.instabugBridge networkLogIOS:url method:method requestBody:requestBody @@ -348,8 +353,11 @@ - (void)testNetworkLogIOS { startTime:startTime duration:duration gqlQueryName:gqlQueryName - serverErrorMessage:serverErrorMessage]; - + serverErrorMessage:serverErrorMessage + w3cExternalTraceAttributes:w3cExternalTraceAttributes + + ]; + OCMVerify([mIBGNetworkLogger addNetworkLogWithUrl:url method:method requestBody:requestBody @@ -366,11 +374,12 @@ - (void)testNetworkLogIOS { duration:duration * 1000 gqlQueryName:gqlQueryName serverErrorMessage:serverErrorMessage - isW3cCaughted:nil - partialID:nil - timestamp:nil - generatedW3CTraceparent:nil - caughtedW3CTraceparent:nil]); + isW3cCaughted:isW3cCaughted + partialID:partialID + timestamp:timestamp + generatedW3CTraceparent:generatedW3CTraceparent + caughtedW3CTraceparent:caughtedW3CTraceparent + ]); } - (void)testSetFileAttachment { @@ -541,4 +550,63 @@ - (void)testRemoveAllFeatureFlags { OCMVerify([mock removeAllFeatureFlags]); } + +- (void) testIsW3ExternalTraceIDEnabled { + id mock = OCMClassMock([IBGNetworkLogger class]); + NSNumber *expectedValue = @(YES); + + OCMStub([mock w3ExternalTraceIDEnabled]).andReturn([expectedValue boolValue]); + + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + RCTPromiseResolveBlock resolve = ^(NSNumber *result) { + XCTAssertEqualObjects(result, expectedValue); + [expectation fulfill]; + }; + + [self.instabugBridge isW3ExternalTraceIDEnabled:resolve :nil]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + OCMVerify([mock w3ExternalTraceIDEnabled]); +} + +- (void) testIsW3ExternalGeneratedHeaderEnabled { + id mock = OCMClassMock([IBGNetworkLogger class]); + NSNumber *expectedValue = @(YES); + + OCMStub([mock w3ExternalGeneratedHeaderEnabled]).andReturn([expectedValue boolValue]); + + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + RCTPromiseResolveBlock resolve = ^(NSNumber *result) { + XCTAssertEqualObjects(result, expectedValue); + [expectation fulfill]; + }; + + [self.instabugBridge isW3ExternalGeneratedHeaderEnabled:resolve :nil]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + OCMVerify([mock w3ExternalGeneratedHeaderEnabled]); +} + +- (void) testIsW3CaughtHeaderEnabled { + id mock = OCMClassMock([IBGNetworkLogger class]); + NSNumber *expectedValue = @(YES); + + OCMStub([mock w3CaughtHeaderEnabled]).andReturn([expectedValue boolValue]); + + XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; + RCTPromiseResolveBlock resolve = ^(NSNumber *result) { + XCTAssertEqualObjects(result, expectedValue); + [expectation fulfill]; + }; + + [self.instabugBridge isW3CaughtHeaderEnabled:resolve :nil]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + + OCMVerify([mock w3CaughtHeaderEnabled]); +} + + @end diff --git a/examples/default/ios/InstabugTests/RNInstabugTests.m b/examples/default/ios/InstabugTests/RNInstabugTests.m index cde248ad30..930da52ca5 100644 --- a/examples/default/ios/InstabugTests/RNInstabugTests.m +++ b/examples/default/ios/InstabugTests/RNInstabugTests.m @@ -69,7 +69,7 @@ - (void)testInitWithLogsLevel { - (void) testSetCodePushVersion { NSString *codePushVersion = @"1.0.0(1)"; [RNInstabug setCodePushVersion:codePushVersion]; - + OCMVerify([self.mInstabug setCodePushVersion:codePushVersion]); } diff --git a/examples/default/src/screens/apm/NetworkScreen.tsx b/examples/default/src/screens/apm/NetworkScreen.tsx index 8aa20f49f0..f9f057f612 100644 --- a/examples/default/src/screens/apm/NetworkScreen.tsx +++ b/examples/default/src/screens/apm/NetworkScreen.tsx @@ -94,6 +94,14 @@ export const NetworkScreen: React.FC< }; const { data, isError, isSuccess, isLoading, refetch } = useQuery('helloQuery', fetchGraphQlData); + const simulateNetworkRequest = () => { + axios.get('https://httpbin.org/anything', { + headers: { traceparent: 'Caught Header Example' }, + }); + }; + const simulateNetworkRequestWithoutHeader = () => { + axios.get('https://httpbin.org/anything'); + }; return ( @@ -111,7 +119,14 @@ export const NetworkScreen: React.FC< onPress={sendRequestToUrlUsingAxios} title="Send Request To Url Using Axios" /> - + simulateNetworkRequest()} + /> + simulateNetworkRequestWithoutHeader()} + /> refetch} title="Reload GraphQL" /> {isLoading && Loading...} diff --git a/ios/RNInstabug/InstabugAPMBridge.m b/ios/RNInstabug/InstabugAPMBridge.m index 0324be4a8b..daea8b4c1a 100644 --- a/ios/RNInstabug/InstabugAPMBridge.m +++ b/ios/RNInstabug/InstabugAPMBridge.m @@ -8,6 +8,7 @@ #import #import #import +#import "Util/IBGAPM+PrivateAPIs.h" @implementation InstabugAPMBridge @@ -110,6 +111,9 @@ - (id) init } + + + @synthesize description; @synthesize hash; diff --git a/ios/RNInstabug/InstabugReactBridge.h b/ios/RNInstabug/InstabugReactBridge.h index a3cfc21c13..bca04ddfd0 100644 --- a/ios/RNInstabug/InstabugReactBridge.h +++ b/ios/RNInstabug/InstabugReactBridge.h @@ -105,7 +105,9 @@ */ - (void)setNetworkLoggingEnabled:(BOOL)isEnabled; - +- (void)isW3ExternalTraceIDEnabled:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject; +- (void)isW3ExternalGeneratedHeaderEnabled:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject; +- (void)isW3CaughtHeaderEnabled:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject; - (void)networkLogIOS:(NSString * _Nonnull)url method:(NSString * _Nonnull)method requestBody:(NSString * _Nonnull)requestBody @@ -121,7 +123,8 @@ startTime:(double)startTime duration:(double)duration gqlQueryName:(NSString * _Nullable)gqlQueryName - serverErrorMessage:(NSString * _Nullable)serverErrorMessage; + serverErrorMessage:(NSString * _Nullable)serverErrorMessage +w3cExternalTraceAttributes:(NSDictionary * _Nullable)w3cExternalTraceAttributes; /* +------------------------------------------------------------------------+ diff --git a/ios/RNInstabug/InstabugReactBridge.m b/ios/RNInstabug/InstabugReactBridge.m index 534b849081..e7ca15600e 100644 --- a/ios/RNInstabug/InstabugReactBridge.m +++ b/ios/RNInstabug/InstabugReactBridge.m @@ -298,7 +298,14 @@ - (dispatch_queue_t)methodQueue { startTime:(double)startTime duration:(double)duration gqlQueryName:(NSString * _Nullable)gqlQueryName - serverErrorMessage:(NSString * _Nullable)serverErrorMessage) { + serverErrorMessage:(NSString * _Nullable)serverErrorMessage + w3cExternalTraceAttributes:(NSDictionary * _Nullable)w3cExternalTraceAttributes){ + NSNumber *isW3cCaught = (w3cExternalTraceAttributes[@"isW3cHeaderFound"] != [NSNull null]) ? w3cExternalTraceAttributes[@"isW3cHeaderFound"] : nil; + NSNumber * partialID = (w3cExternalTraceAttributes[@"partialId"] != [NSNull null]) ? w3cExternalTraceAttributes[@"partialId"] : nil; + NSNumber * timestamp = (w3cExternalTraceAttributes[@"networkStartTimeInSeconds"] != [NSNull null]) ? w3cExternalTraceAttributes[@"networkStartTimeInSeconds"] : nil; + NSString * generatedW3CTraceparent = (w3cExternalTraceAttributes[@"w3cGeneratedHeader"] != [NSNull null]) ? w3cExternalTraceAttributes[@"w3cGeneratedHeader"] : nil; + NSString * caughtW3CTraceparent = (w3cExternalTraceAttributes[@"w3cCaughtHeader"] != [NSNull null]) ? w3cExternalTraceAttributes[@"w3cCaughtHeader"] : nil; + [IBGNetworkLogger addNetworkLogWithUrl:url method:method requestBody:requestBody @@ -315,11 +322,12 @@ - (dispatch_queue_t)methodQueue { duration:duration * 1000 gqlQueryName:gqlQueryName serverErrorMessage:serverErrorMessage - isW3cCaughted:nil - partialID:nil - timestamp:nil - generatedW3CTraceparent:nil - caughtedW3CTraceparent:nil]; + isW3cCaughted:isW3cCaught + partialID:partialID + timestamp:timestamp + generatedW3CTraceparent:generatedW3CTraceparent + caughtedW3CTraceparent:caughtW3CTraceparent + ]; } RCT_EXPORT_METHOD(addPrivateView: (nonnull NSNumber *)reactTag) { @@ -369,7 +377,7 @@ - (dispatch_queue_t)methodQueue { [featureFlags addObject:[[IBGFeatureFlag alloc] initWithName:key variant:variant]]; } } - + [Instabug addFeatureFlags:featureFlags]; } @@ -378,7 +386,7 @@ - (dispatch_queue_t)methodQueue { for(id item in featureFlags){ [features addObject:[[IBGFeatureFlag alloc] initWithName:item]]; } - + @try { [Instabug removeFeatureFlags:features]; } @@ -395,6 +403,17 @@ - (dispatch_queue_t)methodQueue { [Instabug willRedirectToAppStore]; } +RCT_EXPORT_METHOD(isW3ExternalTraceIDEnabled:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) { + resolve(@(IBGNetworkLogger.w3ExternalTraceIDEnabled)); +} +RCT_EXPORT_METHOD(isW3ExternalGeneratedHeaderEnabled:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) { + resolve(@(IBGNetworkLogger.w3ExternalGeneratedHeaderEnabled)); +} +RCT_EXPORT_METHOD(isW3CaughtHeaderEnabled:(RCTPromiseResolveBlock)resolve :(RCTPromiseRejectBlock)reject) { + resolve(@(IBGNetworkLogger.w3CaughtHeaderEnabled)); +} + + - (NSDictionary *)constantsToExport { return ArgsRegistry.getAll; } diff --git a/ios/RNInstabug/Util/IBGAPM+PrivateAPIs.h b/ios/RNInstabug/Util/IBGAPM+PrivateAPIs.h new file mode 100644 index 0000000000..a451a0ad50 --- /dev/null +++ b/ios/RNInstabug/Util/IBGAPM+PrivateAPIs.h @@ -0,0 +1,15 @@ +// +// IBGAPM+PrivateAPIs.h +// Pods +// +// Created by Instabug on 02/06/2024. +// + +//#import "IBGAPM.h" + +@interface IBGAPM (PrivateAPIs) + +@property (class, atomic, assign) BOOL networkEnabled; + + +@end diff --git a/ios/RNInstabug/Util/IBGNetworkLogger+CP.h b/ios/RNInstabug/Util/IBGNetworkLogger+CP.h index 436553620e..805591d0ce 100644 --- a/ios/RNInstabug/Util/IBGNetworkLogger+CP.h +++ b/ios/RNInstabug/Util/IBGNetworkLogger+CP.h @@ -4,6 +4,11 @@ NS_ASSUME_NONNULL_BEGIN @interface IBGNetworkLogger (CP) +@property (class, atomic, assign) BOOL w3ExternalTraceIDEnabled; +@property (class, atomic, assign) BOOL w3ExternalGeneratedHeaderEnabled; +@property (class, atomic, assign) BOOL w3CaughtHeaderEnabled; + + + (void)disableAutomaticCapturingOfNetworkLogs; + (void)addNetworkLogWithUrl:(NSString *_Nonnull)url method:(NSString *_Nonnull)method @@ -27,6 +32,28 @@ NS_ASSUME_NONNULL_BEGIN generatedW3CTraceparent:(NSString * _Nullable)generatedW3CTraceparent caughtedW3CTraceparent:(NSString * _Nullable)caughtedW3CTraceparent; ++ (void)addNetworkLogWithUrl:(NSString *)url + method:(NSString *)method + requestBody:(NSString *)request + requestBodySize:(int64_t)requestBodySize + responseBody:(NSString *)response + responseBodySize:(int64_t)responseBodySize + responseCode:(int32_t)code + requestHeaders:(NSDictionary *)requestHeaders + responseHeaders:(NSDictionary *)responseHeaders + contentType:(NSString *)contentType + errorDomain:(NSString *)errorDomain + errorCode:(int32_t)errorCode + startTime:(int64_t)startTime + duration:(int64_t) duration + gqlQueryName:(NSString * _Nullable)gqlQueryName + serverErrorMessage:(NSString * _Nullable)serverErrorMessage + isW3cCaughted:(NSNumber * _Nullable)isW3cCaughted + partialID:(NSNumber * _Nullable)partialID + timestamp:(NSNumber * _Nullable)timestamp + generatedW3CTraceparent:(NSString * _Nullable)generatedW3CTraceparent + caughtedW3CTraceparent:(NSString * _Nullable)caughtedW3CTraceparent; + @end NS_ASSUME_NONNULL_END diff --git a/src/models/W3cExternalTraceAttributes.ts b/src/models/W3cExternalTraceAttributes.ts new file mode 100644 index 0000000000..f4e7ab6a45 --- /dev/null +++ b/src/models/W3cExternalTraceAttributes.ts @@ -0,0 +1,22 @@ +export type W3cExternalTraceAttributes = { + /** + * A key that determines if the traceparent header was found + */ + isW3cHeaderFound: boolean | null; + /** + * A unique identifier for the trace generated by the SDK in case of no cought header found + */ + partialId: number | null; + /** + * The start time of the network request + */ + networkStartTimeInSeconds: number | null; + /** + * The traceparent header generated by the SDK + */ + w3cGeneratedHeader: string | null; + /** + * The traceparent header received by the server + */ + w3cCaughtHeader: string | null; +}; diff --git a/src/modules/Instabug.ts b/src/modules/Instabug.ts index 1d528fba94..1c1664ba41 100644 --- a/src/modules/Instabug.ts +++ b/src/modules/Instabug.ts @@ -10,7 +10,8 @@ import type { NavigationAction, NavigationState as NavigationStateV4 } from 'rea import type { InstabugConfig } from '../models/InstabugConfig'; import Report from '../models/Report'; -import { NativeEvents, NativeInstabug, emitter } from '../native/NativeInstabug'; +import { emitter, NativeEvents, NativeInstabug } from '../native/NativeInstabug'; +import { registerW3CFlagsListener } from '../utils/FeatureFlags'; import { ColorTheme, Locale, @@ -68,6 +69,10 @@ export const init = (config: InstabugConfig) => { InstabugUtils.captureJsErrors(); captureUnhandledRejections(); + if (Platform.OS === 'android') { + registerW3CFlagsListener(); + } + // Default networkInterceptionMode to JavaScript if (config.networkInterceptionMode == null) { config.networkInterceptionMode = NetworkInterceptionMode.javascript; @@ -644,3 +649,20 @@ export const componentDidAppearListener = (event: ComponentDidAppearEvent) => { _lastScreen = event.componentName; } }; + +/** + * Sets listener to W3ExternalTraceID flag changes + * @param handler A callback that gets the update value of the flag + */ +export const _registerW3CFlagsChangeListener = ( + handler: (payload: { + isW3ExternalTraceIDEnabled: boolean; + isW3ExternalGeneratedHeaderEnabled: boolean; + isW3CaughtHeaderEnabled: boolean; + }) => void, +) => { + emitter.addListener(NativeEvents.ON_W3C_FLAGS_CHANGE, (payload) => { + handler(payload); + }); + NativeInstabug.registerW3CFlagsChangeListener(); +}; diff --git a/src/native/NativeAPM.ts b/src/native/NativeAPM.ts index b1981cfe37..9fa30b702c 100644 --- a/src/native/NativeAPM.ts +++ b/src/native/NativeAPM.ts @@ -1,5 +1,7 @@ import type { NativeModule } from 'react-native'; +import { NativeEventEmitter } from 'react-native'; +import type { W3cExternalTraceAttributes } from '../models/W3cExternalTraceAttributes'; import { NativeModules } from './NativePackage'; export interface ApmNativeModule extends NativeModule { @@ -22,6 +24,7 @@ export interface ApmNativeModule extends NativeModule { statusCode: number, responseContentType: string, errorDomain: string, + w3cExternalTraceAttributes: W3cExternalTraceAttributes, gqlQueryName?: string, serverErrorMessage?: string, ): void; @@ -48,3 +51,5 @@ export interface ApmNativeModule extends NativeModule { } export const NativeAPM = NativeModules.IBGAPM; + +export const emitter = new NativeEventEmitter(NativeAPM); diff --git a/src/native/NativeInstabug.ts b/src/native/NativeInstabug.ts index 3b72f5951f..5f0628ef71 100644 --- a/src/native/NativeInstabug.ts +++ b/src/native/NativeInstabug.ts @@ -11,6 +11,7 @@ import type { WelcomeMessageMode, } from '../utils/Enums'; import type { NativeConstants } from './NativeConstants'; +import type { W3cExternalTraceAttributes } from '../models/W3cExternalTraceAttributes'; import { NativeModules } from './NativePackage'; export interface InstabugNativeModule extends NativeModule { @@ -67,6 +68,7 @@ export interface InstabugNativeModule extends NativeModule { duration: number, gqlQueryName: string | undefined, serverErrorMessage: string | undefined, + W3cExternalTraceAttributes: W3cExternalTraceAttributes, ): void; setNetworkLoggingEnabled(isEnabled: boolean): void; @@ -140,12 +142,23 @@ export interface InstabugNativeModule extends NativeModule { addFileAttachmentWithURLToReport(url: string, filename?: string): void; addFileAttachmentWithDataToReport(data: string, filename?: string): void; willRedirectToStore(): void; + + // W3C Feature Flags + isW3ExternalTraceIDEnabled(): Promise; + + isW3ExternalGeneratedHeaderEnabled(): Promise; + + isW3CaughtHeaderEnabled(): Promise; + + // W3C Feature Flags Listener for Android + registerW3CFlagsChangeListener(): void; } export const NativeInstabug = NativeModules.Instabug; export enum NativeEvents { PRESENDING_HANDLER = 'IBGpreSendingHandler', + ON_W3C_FLAGS_CHANGE = 'IBGOnNewW3CFlagsUpdateReceivedCallback', } export const emitter = new NativeEventEmitter(NativeInstabug); diff --git a/src/utils/FeatureFlags.ts b/src/utils/FeatureFlags.ts new file mode 100644 index 0000000000..479ab7ba47 --- /dev/null +++ b/src/utils/FeatureFlags.ts @@ -0,0 +1,28 @@ +import { NativeInstabug } from '../native/NativeInstabug'; +import { _registerW3CFlagsChangeListener } from '../modules/Instabug'; + +export const FeatureFlags = { + isW3ExternalTraceID: () => NativeInstabug.isW3ExternalTraceIDEnabled(), + isW3ExternalGeneratedHeader: () => NativeInstabug.isW3ExternalGeneratedHeaderEnabled(), + isW3CaughtHeader: () => NativeInstabug.isW3CaughtHeaderEnabled(), +}; + +export const registerW3CFlagsListener = () => { + _registerW3CFlagsChangeListener( + (res: { + isW3ExternalTraceIDEnabled: boolean; + isW3ExternalGeneratedHeaderEnabled: boolean; + isW3CaughtHeaderEnabled: boolean; + }) => { + FeatureFlags.isW3ExternalTraceID = async () => { + return res.isW3ExternalTraceIDEnabled; + }; + FeatureFlags.isW3ExternalGeneratedHeader = async () => { + return res.isW3ExternalGeneratedHeaderEnabled; + }; + FeatureFlags.isW3CaughtHeader = async () => { + return res.isW3CaughtHeaderEnabled; + }; + }, + ); +}; diff --git a/src/utils/InstabugUtils.ts b/src/utils/InstabugUtils.ts index d4238f14f0..df19f0d42a 100644 --- a/src/utils/InstabugUtils.ts +++ b/src/utils/InstabugUtils.ts @@ -126,6 +126,44 @@ export async function sendCrashReport( return remoteSenderCallback(jsonObject); } +/** + * Generate random 32 bit unsigned integer Hexadecimal (8 chars) lower case letters + * Should not return all zeros + */ +export const generateTracePartialId = () => { + let randomNumber: number; + let hexString: string; + + do { + randomNumber = Math.floor(Math.random() * 0xffffffff); + hexString = randomNumber.toString(16).padStart(8, '0'); + } while (hexString === '00000000'); + + return { numberPartilId: randomNumber, hexStringPartialId: hexString.toLowerCase() }; +}; +/** + * Generate W3C header in the format of {version}-{trace-id}-{parent-id}-{trace-flag} + * @param networkStartTime + * @returns w3c header + */ +export const generateW3CHeader = (networkStartTime: number) => { + const { hexStringPartialId, numberPartilId } = generateTracePartialId(); + + const TRACESTATE = '4942472d'; + const VERSION = '00'; + const TRACE_FLAG = '01'; + + const timestampInSeconds = Math.floor(networkStartTime.valueOf() / 1000); + const hexaDigitsTimestamp = timestampInSeconds.toString(16).toLowerCase(); + const traceId = `${hexaDigitsTimestamp}${hexStringPartialId}${hexaDigitsTimestamp}${hexStringPartialId}`; + const parentId = `${TRACESTATE}${hexStringPartialId}`; + + return { + timestampInSeconds, + partialId: numberPartilId, + w3cHeader: `${VERSION}-${traceId}-${parentId}-${TRACE_FLAG}`, + }; +}; export function isContentTypeNotAllowed(contentType: string) { const allowed = [ @@ -171,6 +209,13 @@ export function reportNetworkLog(network: NetworkData) { network.responseCode, network.contentType, network.errorDomain, + { + isW3cHeaderFound: network.isW3cHeaderFound, + partialId: network.partialId, + networkStartTimeInSeconds: network.networkStartTimeInSeconds, + w3cGeneratedHeader: network.w3cGeneratedHeader, + w3cCaughtHeader: network.w3cCaughtHeader, + }, network.gqlQueryName, network.serverErrorMessage, ); @@ -192,6 +237,13 @@ export function reportNetworkLog(network: NetworkData) { network.duration, network.gqlQueryName, network.serverErrorMessage, + { + isW3cHeaderFound: network.isW3cHeaderFound, + partialId: network.partialId, + networkStartTimeInSeconds: network.networkStartTimeInSeconds, + w3cGeneratedHeader: network.w3cGeneratedHeader, + w3cCaughtHeader: network.w3cCaughtHeader, + }, ); } } @@ -204,4 +256,6 @@ export default { getStackTrace, stringifyIfNotString, sendCrashReport, + generateTracePartialId, + generateW3CHeader, }; diff --git a/src/utils/XhrNetworkInterceptor.ts b/src/utils/XhrNetworkInterceptor.ts index 98c5ef9cc5..4443940362 100644 --- a/src/utils/XhrNetworkInterceptor.ts +++ b/src/utils/XhrNetworkInterceptor.ts @@ -1,5 +1,7 @@ import InstabugConstants from './InstabugConstants'; -import { stringifyIfNotString } from './InstabugUtils'; +import { stringifyIfNotString, generateW3CHeader } from './InstabugUtils'; + +import { FeatureFlags } from '../utils/FeatureFlags'; export type ProgressCallback = (totalBytesSent: number, totalBytesExpectedToSend: number) => void; export type NetworkDataCallback = (data: NetworkData) => void; @@ -22,6 +24,11 @@ export interface NetworkData { gqlQueryName?: string; serverErrorMessage: string; requestContentType: string; + isW3cHeaderFound: boolean | null; + partialId: number | null; + networkStartTimeInSeconds: number | null; + w3cGeneratedHeader: string | null; + w3cCaughtHeader: string | null; } const XMLHttpRequest = global.XMLHttpRequest; @@ -53,8 +60,85 @@ const _reset = () => { gqlQueryName: '', serverErrorMessage: '', requestContentType: '', + isW3cHeaderFound: null, + partialId: null, + networkStartTimeInSeconds: null, + w3cGeneratedHeader: null, + w3cCaughtHeader: null, }; }; +const getTraceparentHeader = async (networkData: NetworkData) => { + const [ + isW3cExternalTraceIDEnabled, + isW3cExternalGeneratedHeaderEnabled, + isW3cCaughtHeaderEnabled, + ] = await Promise.all([ + FeatureFlags.isW3ExternalTraceID(), + FeatureFlags.isW3ExternalGeneratedHeader(), + FeatureFlags.isW3CaughtHeader(), + ]); + + return injectHeaders(networkData, { + isW3cExternalTraceIDEnabled, + isW3cExternalGeneratedHeaderEnabled, + isW3cCaughtHeaderEnabled, + }); +}; + +export const injectHeaders = async ( + networkData: NetworkData, + featureFlags: { + isW3cExternalTraceIDEnabled: boolean; + isW3cExternalGeneratedHeaderEnabled: boolean; + isW3cCaughtHeaderEnabled: boolean; + }, +) => { + const { + isW3cExternalTraceIDEnabled, + isW3cExternalGeneratedHeaderEnabled, + isW3cCaughtHeaderEnabled, + } = featureFlags; + + if (!isW3cExternalTraceIDEnabled) { + return; + } + + const isHeaderFound = networkData.requestHeaders.traceparent != null; + + networkData.isW3cHeaderFound = isHeaderFound; + + const injectionMethodology = isHeaderFound + ? identifyCaughtHeader(networkData, isW3cCaughtHeaderEnabled) + : injectGeneratedData(networkData, isW3cExternalGeneratedHeaderEnabled); + return injectionMethodology; +}; + +const identifyCaughtHeader = async ( + networkData: NetworkData, + isW3cCaughtHeaderEnabled: boolean, +) => { + if (isW3cCaughtHeaderEnabled) { + networkData.w3cCaughtHeader = networkData.requestHeaders.traceparent; + return networkData.requestHeaders.traceparent; + } + return; +}; + +const injectGeneratedData = ( + networkData: NetworkData, + isW3cExternalGeneratedHeaderEnabled: boolean, +) => { + const { timestampInSeconds, partialId, w3cHeader } = generateW3CHeader(networkData.startTime); + networkData.partialId = partialId; + networkData.networkStartTimeInSeconds = timestampInSeconds; + + if (isW3cExternalGeneratedHeaderEnabled) { + networkData.w3cGeneratedHeader = w3cHeader; + return w3cHeader; + } + + return; +}; export default { setOnDoneCallback(callback: NetworkDataCallback) { @@ -91,7 +175,7 @@ export default { originalXHRSetRequestHeader.apply(this, [header, value]); }; - XMLHttpRequest.prototype.send = function (data) { + XMLHttpRequest.prototype.send = async function (data) { const cloneNetwork = JSON.parse(JSON.stringify(network)); cloneNetwork.requestBody = data ? data : ''; @@ -226,6 +310,10 @@ export default { } cloneNetwork.startTime = Date.now(); + const traceparent = await getTraceparentHeader(cloneNetwork); + if (traceparent) { + this.setRequestHeader('Traceparent', traceparent); + } originalXHRSend.apply(this, [data]); }; isInterceptorEnabled = true; diff --git a/test/mocks/mockInstabug.ts b/test/mocks/mockInstabug.ts index 5139afcde3..7b3cf2e695 100644 --- a/test/mocks/mockInstabug.ts +++ b/test/mocks/mockInstabug.ts @@ -69,6 +69,10 @@ const mockInstabug: InstabugNativeModule = { addFileAttachmentWithDataToReport: jest.fn(), setNetworkLoggingEnabled: jest.fn(), willRedirectToStore: jest.fn(), + isW3ExternalTraceIDEnabled: jest.fn(), + isW3ExternalGeneratedHeaderEnabled: jest.fn(), + isW3CaughtHeaderEnabled: jest.fn(), + registerW3CFlagsChangeListener: jest.fn(), }; export default mockInstabug; diff --git a/test/modules/Instabug.spec.ts b/test/modules/Instabug.spec.ts index 46b4b208e1..9db21e3e21 100644 --- a/test/modules/Instabug.spec.ts +++ b/test/modules/Instabug.spec.ts @@ -870,4 +870,20 @@ describe('Instabug Module', () => { Instabug.willRedirectToStore(); expect(NativeInstabug.willRedirectToStore).toBeCalledTimes(1); }); + + it('should register W3C flag listener', async () => { + const callback = jest.fn(); + Instabug._registerW3CFlagsChangeListener(callback); + + expect(NativeInstabug.registerW3CFlagsChangeListener).toBeCalledTimes(1); + }); + + it('should invoke callback on emitting the event IBGOnNewW3CFlagsUpdateReceivedCallback', () => { + const callback = jest.fn(); + Instabug._registerW3CFlagsChangeListener(callback); + emitter.emit(NativeEvents.ON_W3C_FLAGS_CHANGE); + + expect(emitter.listenerCount(NativeEvents.ON_W3C_FLAGS_CHANGE)).toBe(1); + expect(callback).toHaveBeenCalled(); + }); }); diff --git a/test/modules/NetworkLogger.spec.ts b/test/modules/NetworkLogger.spec.ts index 71dd2dd778..be258d7403 100644 --- a/test/modules/NetworkLogger.spec.ts +++ b/test/modules/NetworkLogger.spec.ts @@ -30,6 +30,11 @@ describe('NetworkLogger Module', () => { startTime: 0, serverErrorMessage: '', requestContentType: 'application/json', + isW3cHeaderFound: null, + partialId: null, + networkStartTimeInSeconds: null, + w3cGeneratedHeader: null, + w3cCaughtHeader: null, }; beforeEach(() => { diff --git a/test/utils/InstabugUtils.spec.ts b/test/utils/InstabugUtils.spec.ts index becfccc0e9..fd389d7f0b 100644 --- a/test/utils/InstabugUtils.spec.ts +++ b/test/utils/InstabugUtils.spec.ts @@ -258,6 +258,11 @@ describe('reportNetworkLog', () => { errorDomain: 'errorDomain', serverErrorMessage: 'serverErrorMessage', requestContentType: 'requestContentType', + isW3cHeaderFound: null, + partialId: null, + networkStartTimeInSeconds: null, + w3cGeneratedHeader: null, + w3cCaughtHeader: null, }; it('reportNetworkLog should send network logs to native with the correct parameters on Android', () => { @@ -296,6 +301,13 @@ describe('reportNetworkLog', () => { network.responseCode, network.contentType, network.errorDomain, + { + isW3cHeaderFound: null, + partialId: null, + networkStartTimeInSeconds: null, + w3cGeneratedHeader: null, + w3cCaughtHeader: null, + }, network.gqlQueryName, network.serverErrorMessage, ); @@ -324,6 +336,13 @@ describe('reportNetworkLog', () => { network.duration, network.gqlQueryName, network.serverErrorMessage, + { + isW3cHeaderFound: null, + partialId: null, + networkStartTimeInSeconds: null, + w3cGeneratedHeader: null, + w3cCaughtHeader: null, + }, ); }); }); diff --git a/test/utils/XhrNetworkInterceptor.spec.ts b/test/utils/XhrNetworkInterceptor.spec.ts index 10a8f1abb8..dfb9e7f43b 100644 --- a/test/utils/XhrNetworkInterceptor.spec.ts +++ b/test/utils/XhrNetworkInterceptor.spec.ts @@ -4,7 +4,7 @@ import nock from 'nock'; import waitForExpect from 'wait-for-expect'; import InstabugConstants from '../../src/utils/InstabugConstants'; -import Interceptor from '../../src/utils/XhrNetworkInterceptor'; +import Interceptor, { injectHeaders } from '../../src/utils/XhrNetworkInterceptor'; const url = 'http://api.instabug.com'; const method = 'GET'; @@ -293,3 +293,203 @@ describe('Network Interceptor', () => { FakeRequest.send(); }); }); + +describe('Network Interceptor W3C Headers', () => { + beforeEach(() => { + nock.cleanAll(); + }); + + it('should attach generated header if all flags are enabled on no header found', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: true, + isW3cExternalGeneratedHeaderEnabled: true, + isW3cCaughtHeaderEnabled: true, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(false); + expect(network.partialId).not.toBe(null); + expect(network.networkStartTimeInSeconds).toEqual(Math.floor(network.startTime / 1000)); + expect(network.w3cGeneratedHeader).toHaveLength(55); + expect(network.w3cCaughtHeader).toBe(null); + }); + done(); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + it('should attach generated header if key flag & generated header flags are enabled on no header found', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: true, + isW3cExternalGeneratedHeaderEnabled: true, + isW3cCaughtHeaderEnabled: false, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(false); + expect(network.partialId).not.toBe(null); + expect(network.networkStartTimeInSeconds).toEqual(Math.floor(network.startTime / 1000)); + expect(network.w3cGeneratedHeader).toHaveLength(55); + expect(network.w3cCaughtHeader).toBe(null); + }); + done(); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + + it('should not attach headers when key flag is disabled & generated, caught header flags are enabled', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: false, + isW3cExternalGeneratedHeaderEnabled: true, + isW3cCaughtHeaderEnabled: true, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(null); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe(null); + expect(network.requestHeaders).not.toHaveProperty('traceparent'); + + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + it('should not attach headers when all feature flags are disabled', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: false, + isW3cExternalGeneratedHeaderEnabled: false, + isW3cCaughtHeaderEnabled: false, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(null); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe(null); + expect(network.requestHeaders).not.toHaveProperty('traceparent'); + + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + it('should not attach headers when key & caught header flags are disabled and generated header flag is enabled', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: false, + isW3cExternalGeneratedHeaderEnabled: true, + isW3cCaughtHeaderEnabled: false, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(null); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe(null); + expect(network.requestHeaders).not.toHaveProperty('traceparent'); + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + it('should not attach headers when key & generated header flags are disabled and caught header flag is enabled', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: false, + isW3cExternalGeneratedHeaderEnabled: false, + isW3cCaughtHeaderEnabled: true, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(null); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe(null); + expect(network.requestHeaders).not.toHaveProperty('traceparent'); + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + it('should not attach headers when key flag is enabled & generated, caught header flags are disabled on header found', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: true, + isW3cExternalGeneratedHeaderEnabled: false, + isW3cCaughtHeaderEnabled: false, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + network.requestHeaders.traceparent = 'caught traceparent header'; + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toEqual(true); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe(null); + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + + it('should attach caught header if all flags are enabled ', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: true, + isW3cExternalGeneratedHeaderEnabled: true, + isW3cCaughtHeaderEnabled: true, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + network.requestHeaders.traceparent = 'caught traceparent header'; + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(true); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe('caught traceparent header'); + expect(network.requestHeaders).toHaveProperty('traceparent'); + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); + it('should attach caught header if key & caught header flags are enabled and generated header flag is disabled', (done) => { + const featureFlags = { + isW3cExternalTraceIDEnabled: true, + isW3cExternalGeneratedHeaderEnabled: false, + isW3cCaughtHeaderEnabled: true, + }; + Interceptor.enableInterception(); + Interceptor.setOnDoneCallback((network) => { + network.requestHeaders.traceparent = 'caught traceparent header'; + injectHeaders(network, featureFlags); + expect(network.isW3cHeaderFound).toBe(true); + expect(network.partialId).toBe(null); + expect(network.networkStartTimeInSeconds).toBe(null); + expect(network.w3cGeneratedHeader).toBe(null); + expect(network.w3cCaughtHeader).toBe('caught traceparent header'); + expect(network.requestHeaders).toHaveProperty('traceparent'); + done(); + }); + FakeRequest.mockResponse(request); + FakeRequest.open(method, url); + FakeRequest.send(); + }); +});