diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index 3bfa290fe3..0c697f22e5 100644 --- a/.github/workflows/sample-application.yml +++ b/.github/workflows/sample-application.yml @@ -209,7 +209,7 @@ jobs: rn-architecture: 'new' ios-use-frameworks: 'no-frameworks' build-type: 'production' - test-command: 'yarn test-ios' + test-command: 'yarn test-ios-manual' - job-name: 'Test Android Release Manual Init' platform: android diff --git a/samples/react-native/.detoxrc.js b/samples/react-native/.detoxrc.js index 11c535ba65..a3501515a9 100644 --- a/samples/react-native/.detoxrc.js +++ b/samples/react-native/.detoxrc.js @@ -44,13 +44,26 @@ module.exports = { }, }, }, - 'ci.sim': { + 'ci.sim.auto': { device: 'ci.simulator', app: 'ci.ios', testRunner: { args: { $0: 'jest', - config: 'e2e/jest.config.ios.js', + config: 'e2e/jest.config.ios.auto.js', + }, + jest: { + setupTimeout: 120000, + }, + }, + }, + 'ci.sim.manual': { + device: 'ci.simulator', + app: 'ci.ios', + testRunner: { + args: { + $0: 'jest', + config: 'e2e/jest.config.ios.manual.js', }, jest: { setupTimeout: 120000, diff --git a/samples/react-native/e2e/captureAppStartCrash.test.ios.manual.ts b/samples/react-native/e2e/captureAppStartCrash.test.ios.manual.ts new file mode 100644 index 0000000000..ea6750434e --- /dev/null +++ b/samples/react-native/e2e/captureAppStartCrash.test.ios.manual.ts @@ -0,0 +1,121 @@ +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; +import { Envelope, EventItem } from '@sentry/core'; +import { device } from 'detox'; +import { + createSentryServer, + containingEvent, +} from './utils/mockedSentryServer'; +import { getItemOfTypeFrom } from './utils/event'; + +describe('Capture app start crash', () => { + let sentryServer = createSentryServer(); + sentryServer.start(); + + let envelope: Envelope; + + beforeAll(async () => { + const launchConfig = { + launchArgs: { + 0: '--sentry-crash-on-start', + }, + }; + + const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + + device.launchApp(launchConfig); + device.launchApp(launchConfig); // This launch sends the crash event before the app crashes again + + envelope = await envelopePromise; + }); + + afterAll(async () => { + await sentryServer.close(); + }); + + it('envelope contains sdk metadata', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item).toEqual([ + { + length: expect.any(Number), + type: 'event', + }, + expect.objectContaining({ + platform: 'cocoa', + sdk: { + features: [], + integrations: [ + 'SessionReplay', + 'WatchdogTerminationTracking', + 'Screenshot', + 'Crash', + 'ANRTracking', + 'ViewHierarchy', + 'AutoBreadcrumbTracking', + 'AutoSessionTracking', + 'NetworkTracking', + 'AppStartTracking', + 'FramesTracking', + ], + name: 'sentry.cocoa.react-native', + packages: [ + { + name: 'cocoapods:getsentry/sentry.cocoa.react-native', + version: expect.any(String), + }, + { + name: 'npm:@sentry/react-native', + version: expect.any(String), + }, + ], + version: expect.any(String), + }, + tags: { + 'event.environment': 'native', + 'event.origin': 'ios', + }, + }), + ]); + }); + + it('envelope contains the expected exception', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item).toEqual([ + { + length: expect.any(Number), + type: 'event', + }, + expect.objectContaining({ + exception: { + values: expect.arrayContaining([ + expect.objectContaining({ + mechanism: expect.objectContaining({ + handled: false, + meta: { + mach_exception: { + code: 0, + exception: 10, + name: 'EXC_CRASH', + subcode: 0, + }, + signal: { + code: 0, + name: 'SIGABRT', + number: 6, + }, + }, + type: 'nsexception', + }), + stacktrace: expect.objectContaining({ + frames: expect.any(Array), + }), + type: 'CrashOnStart', + value: 'This was intentional test crash before JS started.', + }), + ]), + }, + }), + ]); + }); +}); diff --git a/samples/react-native/e2e/captureMessage.test.android.ts b/samples/react-native/e2e/captureMessage.test.android.ts index d08473ac2b..c48d9553a5 100644 --- a/samples/react-native/e2e/captureMessage.test.android.ts +++ b/samples/react-native/e2e/captureMessage.test.android.ts @@ -3,7 +3,7 @@ import { Envelope, EventItem } from '@sentry/core'; import { device } from 'detox'; import { createSentryServer, - containingEvent, + containingEventWithAndroidMessage, } from './utils/mockedSentryServer'; import { tap } from './utils/tap'; import { getItemOfTypeFrom } from './utils/event'; @@ -17,7 +17,9 @@ describe('Capture message', () => { beforeAll(async () => { await device.launchApp(); - const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + const envelopePromise = sentryServer.waitForEnvelope( + containingEventWithAndroidMessage('Captured message'), + ); await tap('Capture message'); envelope = await envelopePromise; }); diff --git a/samples/react-native/e2e/captureMessage.test.ios.ts b/samples/react-native/e2e/captureMessage.test.ios.ts index 9e3a804881..40cfcf20c1 100644 --- a/samples/react-native/e2e/captureMessage.test.ios.ts +++ b/samples/react-native/e2e/captureMessage.test.ios.ts @@ -3,7 +3,7 @@ import { Envelope, EventItem } from '@sentry/core'; import { device } from 'detox'; import { createSentryServer, - containingEvent, + containingEventWithMessage, } from './utils/mockedSentryServer'; import { tap } from './utils/tap'; import { getItemOfTypeFrom } from './utils/event'; @@ -17,7 +17,9 @@ describe('Capture message', () => { beforeAll(async () => { await device.launchApp(); - const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + const envelopePromise = sentryServer.waitForEnvelope( + containingEventWithMessage('Captured message'), + ); await tap('Capture message'); envelope = await envelopePromise; }); diff --git a/samples/react-native/e2e/envelopeHeader.test.android.ts b/samples/react-native/e2e/envelopeHeader.test.android.ts index 4be175d6c9..dc4a75676d 100644 --- a/samples/react-native/e2e/envelopeHeader.test.android.ts +++ b/samples/react-native/e2e/envelopeHeader.test.android.ts @@ -3,7 +3,7 @@ import { Envelope } from '@sentry/core'; import { device } from 'detox'; import { createSentryServer, - containingEvent, + containingEventWithAndroidMessage, } from './utils/mockedSentryServer'; import { HEADER } from './utils/consts'; import { tap } from './utils/tap'; @@ -17,7 +17,9 @@ describe('Capture message', () => { beforeAll(async () => { await device.launchApp(); - const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + const envelopePromise = sentryServer.waitForEnvelope( + containingEventWithAndroidMessage('Captured message'), + ); await tap('Capture message'); envelope = await envelopePromise; diff --git a/samples/react-native/e2e/envelopeHeader.test.ios.ts b/samples/react-native/e2e/envelopeHeader.test.ios.ts index 04ce534226..5798b07d5d 100644 --- a/samples/react-native/e2e/envelopeHeader.test.ios.ts +++ b/samples/react-native/e2e/envelopeHeader.test.ios.ts @@ -3,7 +3,7 @@ import { Envelope } from '@sentry/core'; import { device } from 'detox'; import { createSentryServer, - containingEvent, + containingEventWithMessage, } from './utils/mockedSentryServer'; import { HEADER } from './utils/consts'; import { tap } from './utils/tap'; @@ -17,7 +17,9 @@ describe('Capture message', () => { beforeAll(async () => { await device.launchApp(); - const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + const envelopePromise = sentryServer.waitForEnvelope( + containingEventWithMessage('Captured message'), + ); await tap('Capture message'); envelope = await envelopePromise; diff --git a/samples/react-native/e2e/jest.config.android.js b/samples/react-native/e2e/jest.config.android.js index 2d755851a0..a07210b212 100644 --- a/samples/react-native/e2e/jest.config.android.js +++ b/samples/react-native/e2e/jest.config.android.js @@ -1,16 +1,7 @@ +const baseConfig = require('./jest.config.base'); + /** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { - preset: 'ts-jest', - rootDir: '..', - testMatch: [ - '/e2e/**/*.test.ts', - '/e2e/**/*.test.android.ts', - ], - testTimeout: 120000, - maxWorkers: 1, - globalSetup: 'detox/runners/jest/globalSetup', - globalTeardown: 'detox/runners/jest/globalTeardown', - reporters: ['detox/runners/jest/reporter'], - testEnvironment: 'detox/runners/jest/testEnvironment', - verbose: true, + ...baseConfig, + testMatch: [...baseConfig.testMatch, '/e2e/**/*.test.android.ts'], }; diff --git a/samples/react-native/e2e/jest.config.js b/samples/react-native/e2e/jest.config.base.js similarity index 100% rename from samples/react-native/e2e/jest.config.js rename to samples/react-native/e2e/jest.config.base.js diff --git a/samples/react-native/e2e/jest.config.ios.auto.js b/samples/react-native/e2e/jest.config.ios.auto.js new file mode 100644 index 0000000000..ebfadcaeb6 --- /dev/null +++ b/samples/react-native/e2e/jest.config.ios.auto.js @@ -0,0 +1,11 @@ +const baseConfig = require('./jest.config.base'); + +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + ...baseConfig, + testMatch: [ + ...baseConfig.testMatch, + '/e2e/**/*.test.ios.ts', + '/e2e/**/*.test.ios.auto.ts', + ], +}; diff --git a/samples/react-native/e2e/jest.config.ios.js b/samples/react-native/e2e/jest.config.ios.js deleted file mode 100644 index 62034bc390..0000000000 --- a/samples/react-native/e2e/jest.config.ios.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @type {import('@jest/types').Config.InitialOptions} */ -module.exports = { - preset: 'ts-jest', - rootDir: '..', - testMatch: ['/e2e/**/*.test.ts', '/e2e/**/*.test.ios.ts'], - testTimeout: 120000, - maxWorkers: 1, - globalSetup: 'detox/runners/jest/globalSetup', - globalTeardown: 'detox/runners/jest/globalTeardown', - reporters: ['detox/runners/jest/reporter'], - testEnvironment: 'detox/runners/jest/testEnvironment', - verbose: true, -}; diff --git a/samples/react-native/e2e/jest.config.ios.manual.js b/samples/react-native/e2e/jest.config.ios.manual.js new file mode 100644 index 0000000000..fe271fca7d --- /dev/null +++ b/samples/react-native/e2e/jest.config.ios.manual.js @@ -0,0 +1,11 @@ +const baseConfig = require('./jest.config.base'); + +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + ...baseConfig, + testMatch: [ + ...baseConfig.testMatch, + '/e2e/**/*.test.ios.ts', + '/e2e/**/*.test.ios.manual.ts', + ], +}; diff --git a/samples/react-native/e2e/utils/mockedSentryServer.ts b/samples/react-native/e2e/utils/mockedSentryServer.ts index 7f19a0d24b..9c738ce6a5 100644 --- a/samples/react-native/e2e/utils/mockedSentryServer.ts +++ b/samples/react-native/e2e/utils/mockedSentryServer.ts @@ -94,6 +94,27 @@ export function containingEvent(envelope: Envelope) { return envelope[1].some(item => itemHeaderIsType(item[0], 'event')); } +export function containingEventWithAndroidMessage(message: string) { + return (envelope: Envelope) => + envelope[1].some( + item => + itemHeaderIsType(item[0], 'event') && + itemBodyIsEvent(item[1]) && + item[1].message && + (item[1].message as unknown as { message: string }).message === message, + ); +} + +export function containingEventWithMessage(message: string) { + return (envelope: Envelope) => + envelope[1].some( + item => + itemHeaderIsType(item[0], 'event') && + itemBodyIsEvent(item[1]) && + item[1].message === message, + ); +} + export function containingTransactionWithName(name: string) { return (envelope: Envelope) => envelope[1].some( diff --git a/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme b/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme index 61b12d2c2c..1b6c7a3f15 100644 --- a/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme +++ b/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme @@ -65,6 +65,10 @@ argument = "--sentry-disable-native-start" isEnabled = "NO"> + + *arguments = [[NSProcessInfo processInfo] arguments]; + return [arguments containsObject:@"--sentry-crash-on-start"]; +} + @end diff --git a/samples/react-native/package.json b/samples/react-native/package.json index a5b36afd9d..8e80a5d598 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -15,7 +15,7 @@ "set-test-dsn-android": "scripts/set-dsn-aos.mjs", "set-test-dsn-ios": "scripts/set-dsn-ios.mjs", "test-android": "scripts/test-android.sh", - "test-ios": "scripts/test-ios.sh", + "test-ios-manual": "scripts/test-ios-manual.sh", "test-ios-auto": "scripts/test-ios-auto.sh", "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx", "fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", diff --git a/samples/react-native/scripts/test-ios-auto.sh b/samples/react-native/scripts/test-ios-auto.sh index 5798457ec2..9192a9619e 100755 --- a/samples/react-native/scripts/test-ios-auto.sh +++ b/samples/react-native/scripts/test-ios-auto.sh @@ -9,4 +9,4 @@ cd "${thisFilePath}/.." "${thisFilePath}/detect-ios-sim.sh" -detox test --configuration ci.sim --app-launch-args="--sentry-disable-native-start" +detox test --configuration ci.sim.auto --app-launch-args="--sentry-disable-native-start" diff --git a/samples/react-native/scripts/test-ios.sh b/samples/react-native/scripts/test-ios-manual.sh similarity index 78% rename from samples/react-native/scripts/test-ios.sh rename to samples/react-native/scripts/test-ios-manual.sh index 3e8d8c64a3..b1c295edda 100755 --- a/samples/react-native/scripts/test-ios.sh +++ b/samples/react-native/scripts/test-ios-manual.sh @@ -9,4 +9,4 @@ cd "${thisFilePath}/.." "${thisFilePath}/detect-ios-sim.sh" -detox test --configuration ci.sim +detox test --configuration ci.sim.manual