diff --git a/CHANGELOG.md b/CHANGELOG.md index 0503f3af9f..cc62b3f6fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,44 @@ ## Unreleased +#### Features + +- Add experimental support for Log tracing ([#4827](https://github.com/getsentry/sentry-react-native/pull/4827)) + +To enable it add the following code to your Sentry Options: + +```typescript +Sentry.init({ + // other options... + _experiments: { + enableLogs: true, + }, +}); +``` + +You can also filter the logs being collected by adding beforeSendLogs into `_experiments` + +```typescript +Sentry.init({ + // other options... + _experiments: { + enableLogs: true, + beforeSendLog: (log) => { + return log; + }, + } +}); +``` + ### Changes - Remove deprecated `appOwnership` constant use in Expo Go detection ([#4893](https://github.com/getsentry/sentry-react-native/pull/4893)) - Disable AppStart and NativeFrames in unsupported environments (web, Expo Go) ([#4897](https://github.com/getsentry/sentry-react-native/pull/4897)) +### Self Hosted + +- It is recommended to use Sentry Self Hosted version `25.2.0` or new for React Native V7 or newer + ## 7.0.0-beta.0 ### Upgrading from 6.x to 7.0 diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index e3856b0646..cb845ad984 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -279,6 +279,12 @@ protected void getSentryAndroidOptions( if (rnOptions.hasKey("enableNdk")) { options.setEnableNdk(rnOptions.getBoolean("enableNdk")); } + if (rnOptions.hasKey("_experiments")) { + ReadableMap experiments = rnOptions.getMap("_experiments"); + if (experiments.hasKey("enableLogs")) { + options.getLogs().setEnabled(experiments.getBoolean("enableLogs")); + } + } if (rnOptions.hasKey("spotlight")) { if (rnOptions.getType("spotlight") == ReadableType.Boolean) { options.setEnableSpotlight(rnOptions.getBoolean("spotlight")); diff --git a/packages/core/src/js/client.ts b/packages/core/src/js/client.ts index 6ed2c172b1..957aae9f5c 100644 --- a/packages/core/src/js/client.ts +++ b/packages/core/src/js/client.ts @@ -11,6 +11,7 @@ import type { UserFeedback, } from '@sentry/core'; import { + _INTERNAL_flushLogsBuffer, addAutoIpAddressToSession, addAutoIpAddressToUser, BaseClient, @@ -31,6 +32,8 @@ import { mergeOutcomes } from './utils/outcome'; import { ReactNativeLibraries } from './utils/rnlibraries'; import { NATIVE } from './wrapper'; +const DEFAULT_FLUSH_INTERVAL = 5000; + /** * The Sentry React Native SDK Client. * @@ -39,6 +42,7 @@ import { NATIVE } from './wrapper'; */ export class ReactNativeClient extends BaseClient { private _outcomesBuffer: Outcome[]; + private _logFlushIdleTimeout: ReturnType | undefined; /** * Creates a new React Native SDK instance. @@ -59,6 +63,22 @@ export class ReactNativeClient extends BaseClient { this.on('postprocessEvent', addAutoIpAddressToUser); this.on('beforeSendSession', addAutoIpAddressToSession); } + + if (options._experiments?.enableLogs) { + this.on('flush', () => { + _INTERNAL_flushLogsBuffer(this); + }); + + this.on('afterCaptureLog', () => { + if (this._logFlushIdleTimeout) { + clearTimeout(this._logFlushIdleTimeout); + } + + this._logFlushIdleTimeout = setTimeout(() => { + _INTERNAL_flushLogsBuffer(this); + }, DEFAULT_FLUSH_INTERVAL); + }); + } } /** diff --git a/packages/core/src/js/options.ts b/packages/core/src/js/options.ts index 9adeb4c187..a9efb341d5 100644 --- a/packages/core/src/js/options.ts +++ b/packages/core/src/js/options.ts @@ -1,6 +1,6 @@ import type { makeFetchTransport } from '@sentry/browser'; import type { CaptureContext, ClientOptions, Event, EventHint, Options } from '@sentry/core'; -import type { Profiler } from '@sentry/react'; +import type { BrowserOptions, Profiler } from '@sentry/react'; import type * as React from 'react'; import { Platform } from 'react-native'; import type { TouchEventBoundaryProps } from './touchevents'; @@ -9,6 +9,9 @@ import { isExpoGo } from './utils/environment'; type ProfilerProps = React.ComponentProps; type BrowserTransportOptions = Parameters[0]; +type BrowserExperiments = NonNullable; +type SharedExperimentsSubset = Pick; + export interface BaseReactNativeOptions { /** * Enables native transport + device info + offline caching. @@ -244,7 +247,7 @@ export interface BaseReactNativeOptions { /** * Options which are in beta, or otherwise not guaranteed to be stable. */ - _experiments?: { + _experiments?: SharedExperimentsSubset & { [key: string]: unknown; /** diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts index 1caeab0eff..a1f49bd496 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -185,7 +185,7 @@ export const NATIVE: SentryNativeWrapper = { typeof itemHeader.content_type === 'string' ? itemHeader.content_type : 'application/octet-stream'; bytesPayload = itemPayload; } else { - bytesContentType = 'application/json'; + bytesContentType = 'application/vnd.sentry.items.log+json'; bytesPayload = encodeUTF8(JSON.stringify(itemPayload)); if (!hardCrashed) { hardCrashed = isHardCrash(itemPayload); diff --git a/packages/core/test/wrapper.test.ts b/packages/core/test/wrapper.test.ts index 80a89abf5a..8e75622805 100644 --- a/packages/core/test/wrapper.test.ts +++ b/packages/core/test/wrapper.test.ts @@ -304,7 +304,7 @@ describe('Tests Native Wrapper', () => { base64StringFromByteArray( utf8ToBytes( '{"event_id":"event0","sent_at":"123"}\n' + - '{"type":"event","content_type":"application/json","length":87}\n' + + '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":87}\n' + '{"event_id":"event0","message":"test","sdk":{"name":"test-sdk-name","version":"2.1.3"}}\n', ), ), @@ -336,7 +336,7 @@ describe('Tests Native Wrapper', () => { base64StringFromByteArray( utf8ToBytes( '{"event_id":"event0","sent_at":"123"}\n' + - '{"type":"event","content_type":"application/json","length":93}\n' + + '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":93}\n' + '{"event_id":"event0","sdk":{"name":"test-sdk-name","version":"2.1.3"},"instance":{"value":0}}\n', ), ), @@ -379,7 +379,7 @@ describe('Tests Native Wrapper', () => { base64StringFromByteArray( utf8ToBytes( '{"event_id":"event0","sent_at":"123"}\n' + - '{"type":"event","content_type":"application/json","length":50}\n' + + '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":50}\n' + '{"event_id":"event0","message":{"message":"test"}}\n', ), ), @@ -418,7 +418,7 @@ describe('Tests Native Wrapper', () => { base64StringFromByteArray( utf8ToBytes( '{"event_id":"event0","sent_at":"123"}\n' + - '{"type":"event","content_type":"application/json","length":124}\n' + + '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":124}\n' + '{"event_id":"event0","exception":{"values":[{"mechanism":{"handled":true,"type":""}}]},"breadcrumbs":[{"message":"crumb!"}]}\n', ), ), @@ -447,7 +447,7 @@ describe('Tests Native Wrapper', () => { base64StringFromByteArray( utf8ToBytes( '{"event_id":"event0","sent_at":"123"}\n' + - '{"type":"event","content_type":"application/json","length":58}\n' + + '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":58}\n' + '{"event_id":"event0","breadcrumbs":[{"message":"crumb!"}]}\n', ), ), @@ -486,7 +486,7 @@ describe('Tests Native Wrapper', () => { base64StringFromByteArray( utf8ToBytes( '{"event_id":"event0","sent_at":"123"}\n' + - '{"type":"event","content_type":"application/json","length":132}\n' + + '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":132}\n' + '{"event_id":"event0","exception":{"values":[{"mechanism":{"handled":false,"type":"onerror"}}]},"breadcrumbs":[{"message":"crumb!"}]}\n', ), ), diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index dd69e76fe0..45b04aafbb 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -76,6 +76,12 @@ Sentry.init({ didCallNativeInit, ); }, + _experiments: { + enableLogs: true, + beforeSendLog: (log) => { + return log; + }, + }, enableUserInteractionTracing: true, integrations(integrations) { integrations.push( diff --git a/samples/react-native/src/Screens/ErrorsScreen.tsx b/samples/react-native/src/Screens/ErrorsScreen.tsx index 107e705a82..4da87d7b25 100644 --- a/samples/react-native/src/Screens/ErrorsScreen.tsx +++ b/samples/react-native/src/Screens/ErrorsScreen.tsx @@ -20,6 +20,7 @@ import { FallbackRender } from '@sentry/react'; import NativeSampleModule from '../../tm/NativeSampleModule'; import NativePlatformSampleModule from '../../tm/NativePlatformSampleModule'; import { TimeToFullDisplay } from '../utils'; +import { logger } from '@sentry/browser'; const { AssetsModule, CppModule, CrashModule } = NativeModules; @@ -150,6 +151,18 @@ const ErrorsScreen = (_props: Props) => { } }} /> +