From 6e8584e8f1988b33cc24c85f32711c91358905fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Wold=C5=99ich?= <31292499+krystofwoldrich@users.noreply.github.com> Date: Mon, 30 Oct 2023 12:21:18 +0100 Subject: [PATCH] feat(sdk): Automatically detect environment if not set by User (#3362) --- CHANGELOG.md | 1 + src/js/profiling/utils.ts | 3 +- src/js/sdk.tsx | 5 ++ src/js/utils/environment.ts | 5 ++ test/profiling/integration.test.ts | 114 +++++++++++++++++++++++++++-- test/sdk.test.ts | 30 ++++++++ 6 files changed, 151 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 376a5214c..4ad8d1f69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Automatically detect environment if not set ([#3362](https://github.com/getsentry/sentry-react-native/pull/3362)) - Send Source Maps Debug ID for symbolicated Profiles ([#3343](https://github.com/getsentry/sentry-react-native/pull/3343)) ### Fixes diff --git a/src/js/profiling/utils.ts b/src/js/profiling/utils.ts index 85230e4ef..1304315e1 100644 --- a/src/js/profiling/utils.ts +++ b/src/js/profiling/utils.ts @@ -1,6 +1,7 @@ import type { DebugImage, Envelope, Event, Profile } from '@sentry/types'; import { forEachEnvelopeItem, GLOBAL_OBJ, logger } from '@sentry/utils'; +import { getDefaultEnvironment } from '../utils/environment'; import { DEFAULT_BUNDLE_NAME } from './hermes'; import type { RawThreadCpuProfile } from './types'; @@ -65,7 +66,7 @@ export function createProfilingEvent(profile: RawThreadCpuProfile, event: Event) return createProfilePayload(profile, { release: event.release || '', - environment: event.environment || '', + environment: event.environment || getDefaultEnvironment(), event_id: event.event_id || '', transaction: event.transaction || '', start_timestamp: event.start_timestamp ? event.start_timestamp * 1000 : Date.now(), diff --git a/src/js/sdk.tsx b/src/js/sdk.tsx index fa7d1bc94..5186694f4 100644 --- a/src/js/sdk.tsx +++ b/src/js/sdk.tsx @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import type { Scope } from '@sentry/core'; import { getIntegrationsToSetup, hasTracingEnabled, Hub, initAndBind, makeMain, setExtra } from '@sentry/core'; import { HttpClient } from '@sentry/integrations'; @@ -33,6 +34,7 @@ import { TouchEventBoundary } from './touchevents'; import { ReactNativeProfiler, ReactNativeTracing } from './tracing'; import { DEFAULT_BUFFER_SIZE, makeNativeTransportFactory } from './transports/native'; import { makeUtf8TextEncoder } from './transports/TextEncoder'; +import { getDefaultEnvironment } from './utils/environment'; import { safeFactory, safeTracesSampler } from './utils/safe'; import { NATIVE } from './wrapper'; @@ -95,6 +97,9 @@ export function init(passedOptions: ReactNativeOptions): void { initialScope: safeFactory(passedOptions.initialScope, { loggerMessage: 'The initialScope threw an error' }), tracesSampler: safeTracesSampler(passedOptions.tracesSampler), }; + if (!('environment' in options)) { + options.environment = getDefaultEnvironment(); + } const defaultIntegrations: Integration[] = passedOptions.defaultIntegrations || []; if (passedOptions.defaultIntegrations === undefined) { diff --git a/src/js/utils/environment.ts b/src/js/utils/environment.ts index 64f0449f9..2520a8e2b 100644 --- a/src/js/utils/environment.ts +++ b/src/js/utils/environment.ts @@ -35,3 +35,8 @@ export function getHermesVersion(): string | undefined { RN_GLOBAL_OBJ.HermesInternal.getRuntimeProperties()['OSS Release Version'] ); } + +/** Returns default environment based on __DEV__ */ +export function getDefaultEnvironment(): 'development' | 'production' { + return typeof __DEV__ !== 'undefined' && __DEV__ ? 'development' : 'production'; +} diff --git a/test/profiling/integration.test.ts b/test/profiling/integration.test.ts index c8559feba..970d791ad 100644 --- a/test/profiling/integration.test.ts +++ b/test/profiling/integration.test.ts @@ -7,8 +7,9 @@ import type { Envelope, Event, Profile, Transaction, Transport } from '@sentry/t import * as Sentry from '../../src/js'; import { HermesProfiling } from '../../src/js/integrations'; +import type { NativeDeviceContextsResponse } from '../../src/js/NativeRNSentry'; import type * as Hermes from '../../src/js/profiling/hermes'; -import { isHermesEnabled } from '../../src/js/utils/environment'; +import { getDefaultEnvironment, isHermesEnabled } from '../../src/js/utils/environment'; import { RN_GLOBAL_OBJ } from '../../src/js/utils/worldwide'; import { MOCK_DSN } from '../mockDsn'; import { envelopeItemPayload, envelopeItems } from '../testutils'; @@ -35,7 +36,7 @@ describe('profiling integration', () => { }); test('should start profile if there is a transaction running when integration is created', () => { - mock = initTestClient(false); + mock = initTestClient({ withProfiling: false }); jest.runAllTimers(); jest.clearAllMocks(); @@ -65,6 +66,96 @@ describe('profiling integration', () => { ]); }); + describe('environment', () => { + beforeEach(() => { + (getDefaultEnvironment as jest.Mock).mockReturnValue('mocked'); + mockWrapper.NATIVE.fetchNativeDeviceContexts.mockResolvedValue({ + environment: 'native', + }); + }); + + const expectTransactionWithEnvironment = (envelope: Envelope | undefined, env: string | undefined) => { + const transactionEvent = envelope?.[envelopeItems][0][envelopeItemPayload] as Event; + expect(transactionEvent).toEqual( + expect.objectContaining>({ + environment: env, + }), + ); + }; + + const expectProfileWithEnvironment = (envelope: Envelope | undefined, env: string | undefined) => { + const profileEvent = (envelope?.[envelopeItems][1] as [{ type: 'profile' }, Profile])[1]; + expect(profileEvent).toEqual( + expect.objectContaining>({ + environment: env, + }), + ); + }; + + test('should use default environment for transaction and profile', () => { + mock = initTestClient(); + + const transaction: Transaction = Sentry.startTransaction({ + name: 'test-name', + }); + transaction.finish(); + + jest.runAllTimers(); + + const envelope: Envelope | undefined = mock.transportSendMock.mock.lastCall?.[0]; + expectTransactionWithEnvironment(envelope, 'mocked'); + expectProfileWithEnvironment(envelope, 'mocked'); + }); + + test('should use native environment for transaction and profile if user value is nullish', () => { + mock = initTestClient({ withProfiling: true, environment: '' }); + + const transaction: Transaction = Sentry.startTransaction({ + name: 'test-name', + }); + transaction.finish(); + + jest.runAllTimers(); + + const envelope: Envelope | undefined = mock.transportSendMock.mock.lastCall?.[0]; + expectTransactionWithEnvironment(envelope, 'native'); + expectProfileWithEnvironment(envelope, 'native'); + }); + + test('should keep nullish for transaction and profile uses default', () => { + mockWrapper.NATIVE.fetchNativeDeviceContexts.mockResolvedValue({ + environment: undefined, + }); + mock = initTestClient({ withProfiling: true, environment: undefined }); + + const transaction: Transaction = Sentry.startTransaction({ + name: 'test-name', + }); + transaction.finish(); + + jest.runAllTimers(); + + const envelope: Envelope | undefined = mock.transportSendMock.mock.lastCall?.[0]; + expectTransactionWithEnvironment(envelope, undefined); + expectProfileWithEnvironment(envelope, 'mocked'); + }); + + test('should keep custom environment for transaction and profile', () => { + mock = initTestClient({ withProfiling: true, environment: 'custom' }); + + const transaction: Transaction = Sentry.startTransaction({ + name: 'test-name', + }); + transaction.finish(); + + jest.runAllTimers(); + + const envelope: Envelope | undefined = mock.transportSendMock.mock.lastCall?.[0]; + expectTransactionWithEnvironment(envelope, 'custom'); + expectProfileWithEnvironment(envelope, 'custom'); + }); + }); + describe('with profiling enabled', () => { beforeEach(() => { mock = initTestClient(); @@ -231,17 +322,24 @@ function getCurrentHermesProfilingIntegration(): TestHermesIntegration { return integration as unknown as TestHermesIntegration; } -function initTestClient(withProfiling: boolean = true): { +function initTestClient( + testOptions: { + withProfiling?: boolean; + environment?: string; + } = { + withProfiling: true, + }, +): { transportSendMock: jest.Mock, Parameters>; } { const transportSendMock = jest.fn, Parameters>(); - Sentry.init({ + const options: Sentry.ReactNativeOptions = { dsn: MOCK_DSN, _experiments: { profilesSampleRate: 1, }, integrations: integrations => { - if (!withProfiling) { + if (!testOptions.withProfiling) { return integrations.filter(i => i.name !== 'HermesProfiling'); } return integrations; @@ -250,7 +348,11 @@ function initTestClient(withProfiling: boolean = true): { send: transportSendMock.mockResolvedValue(undefined), flush: jest.fn().mockResolvedValue(true), }), - }); + }; + if ('environment' in testOptions) { + options.environment = testOptions.environment; + } + Sentry.init(options); // In production integrations are setup only once, but in the tests we want them to setup on every init const integrations = Sentry.getCurrentHub().getClient()?.getOptions().integrations; diff --git a/test/sdk.test.ts b/test/sdk.test.ts index 00ebd6eeb..706512883 100644 --- a/test/sdk.test.ts +++ b/test/sdk.test.ts @@ -184,6 +184,36 @@ describe('Tests the SDK functionality', () => { }); }); + describe('environment', () => { + it('detect development environment', () => { + init({ + enableNative: true, + }); + expect(usedOptions()?.environment).toBe('development'); + }); + + it('uses custom environment', () => { + init({ + environment: 'custom', + }); + expect(usedOptions()?.environment).toBe('custom'); + }); + + it('it keeps empty string environment', () => { + init({ + environment: '', + }); + expect(usedOptions()?.environment).toBe(''); + }); + + it('it keeps undefined environment', () => { + init({ + environment: undefined, + }); + expect(usedOptions()?.environment).toBe(undefined); + }); + }); + describe('transport options buffer size', () => { it('uses default transport options buffer size', () => { init({