diff --git a/CHANGELOG.md b/CHANGELOG.md index b1027c30..2784d098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,13 +18,47 @@ Version 10 of the Sentry JavaScript SDK primarily focuses on upgrading underlyin Version 10 of the SDK is compatible with Sentry self-hosted versions 24.4.2 or higher (unchanged from v9). Lower versions may continue to work, but may not support all features. +### Init changed for Sentry Vue and Nuxt + +Instead of adding the Nuxt/Vue options into Sentry.init options, you will now have to add it inside `siblingOptions`, this only applies to parameters specific to the respective SDK, other SDKs like React or Angular won't have to do that: + +before + +```typescript +Sentry.init({ + app: app, + attachErrorHandler: false, + dsn: '...', + enableLogs: true,... +}, vueInit); +``` + +after + +```typescript +Sentry.init({ + dsn: '...', + enableLogs: true, + siblingOptions: { + vueOptions: { + app: app, + attachErrorHandler: false, + ... + } + }, + ... +}, vueInit); + +``` + ### Features - Add Fallback to JavaScript SDK when Native SDK fails to initialize ([#1043](https://github.com/getsentry/sentry-capacitor/pull/1043)) - Add spotlight integration `spotlightIntegration`. ([#1039](https://github.com/getsentry/sentry-capacitor/pull/1039)) -### Bugfix +### Fixes +- Init now showing the correct JSDoc for Vue/Nuxt init parameters. ([#1046](https://github.com/getsentry/sentry-capacitor/pull/1046)) - Replays/Logs/Sessions now have the `capacitor` SDK name as the source of the event. ([#1043](https://github.com/getsentry/sentry-capacitor/pull/1043)) - Sentry Capacitor integrations are now exposed to `@sentry/capacitor` ([#1039](https://github.com/getsentry/sentry-capacitor/pull/1039)) @@ -56,6 +90,10 @@ Sentry.init({ }); ``` +### Removed Options + +- `_experimental.enableLogs` was removed, please use the options `enableLogs` from `CapacitorOptions`. + For more informations, please go to the following link: https://docs.sentry.io/platforms/javascript/migration/v9-to-v10 ### Dependencies diff --git a/example/ionic-vue3/src/main.ts b/example/ionic-vue3/src/main.ts index 52e21b20..d4b02586 100644 --- a/example/ionic-vue3/src/main.ts +++ b/example/ionic-vue3/src/main.ts @@ -32,7 +32,6 @@ const app = createApp(App) Sentry.init({ - app, dsn: 'https://7f35532db4f8aca7c7b6992d488b39c1@o447951.ingest.sentry.io/4505912397660160', integrations: [ SentryVue.vueIntegration({ @@ -59,11 +58,18 @@ Sentry.init({ enableLogs: true, beforeSendLog: (log) => { return log; - } + }, + siblingOptions: { + vueOptions: { + app: app, + attachErrorHandler: false, + attachProps: false, + }, + }, }, // Forward the init method from @sentry/vue - SentryVue.init + SentryVue.init, ); diff --git a/src/client.ts b/src/client.ts index 02c231ac..5c100bd0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9,7 +9,6 @@ import { NATIVE } from './wrapper'; type BrowserTransportOptions = Parameters[0]; type TransportFactory = (transportOptions: BrowserTransportOptions) => ReturnType; - /** * Post setup the Capacitor client */ @@ -42,7 +41,11 @@ function PostSetupCapacitorClient() : void { * @param originalInit - The original init function to use for the sibling SDK. * @param customTransport - The custom transport to use. */ -export function sdkInit(browserOptions: T & BrowserOptions, nativeOptions: CapacitorOptions, originalInit: (passedOptions: T & BrowserOptions) => void, customTransport?: TransportFactory): void { +export function sdkInit( + browserOptions: BrowserOptions, + nativeOptions: CapacitorOptions, + originalInit: (passedOptions: BrowserOptions) => void, + customTransport?: TransportFactory): void { // We first initialize the NATIVE SDK to avoid the Javascript SDK to invoke any // feature from the NATIVE SDK without the options being set. // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/src/nativeOptions.ts b/src/nativeOptions.ts index e4f8e91b..019adacb 100644 --- a/src/nativeOptions.ts +++ b/src/nativeOptions.ts @@ -1,5 +1,4 @@ import { Capacitor } from '@capacitor/core'; -import type { BrowserOptions } from '@sentry/browser'; import type { CapacitorOptions } from './options'; import { getCurrentSpotlightUrl } from './utils/webViewUrl'; @@ -60,12 +59,10 @@ function iOSParameters(options: CapacitorOptions): CapacitorOptions { : {}; } // Browser options added so options.enableLogs is exposed. -function LogParameters(options: CapacitorOptions & BrowserOptions ): CapacitorLoggerOptions | undefined { - // eslint-disable-next-line deprecation/deprecation - const shouldEnable = options.enableLogs as boolean ?? options._experiments?.enableLogs; +function LogParameters(options: CapacitorOptions & { enableLogs?: boolean }): CapacitorLoggerOptions | undefined { // Only Web and Android implements log parameters initialization. - if (shouldEnable && Capacitor.getPlatform() === 'android') { - return { enableLogs: shouldEnable }; + if (options.enableLogs && Capacitor.getPlatform() === 'android') { + return { enableLogs: true }; } return undefined; } diff --git a/src/options.ts b/src/options.ts index 848a2b46..595c71d8 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,5 +1,6 @@ import type { BrowserOptions, makeFetchTransport } from '@sentry/browser'; import type { ClientOptions } from '@sentry/core'; +import type { NuxtOptions, VueOptions } from './siblingOptions'; // Direct reference of BrowserTransportOptions is not compatible with strict builds of latest versions of Typescript 5. type BrowserTransportOptions = Parameters[0]; @@ -71,13 +72,36 @@ export interface BaseCapacitorOptions { * @default 2 */ appHangTimeoutInterval?: number; + + + /** + * Only for Vue or Nuxt Client. + * Allows the setup of sibling specific SDK. You are still allowed to set the same parameters + * at the root of Capacitor Options at the cost of lost on JSDocs visibility. + */ + siblingOptions?: { + + /** + * Configuration options for the Sentry Vue SDK integration when using Vue. + * These options are passed to the sibling Vue SDK and control Vue-specific features + * such as error handling, component tracing, and props attachment. + */ + vueOptions?: VueOptions; + + /** + * Configuration options for the Sentry Nuxt SDK integration when using Nuxt. + * These options are passed to the sibling Nuxt SDK and control Nuxt-specific Vue features + * such as error handling, component tracing, and props attachment. + */ + nuxtClientOptions?: NuxtOptions; + }; } /** * Configuration options for the Sentry Capacitor SDK. */ export interface CapacitorOptions - extends Omit, + extends Omit, BaseCapacitorOptions { } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/sdk.ts b/src/sdk.ts index 3b3d65ac..5dff1660 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -17,16 +17,19 @@ import { NATIVE } from './wrapper'; * @param options Options for the SDK * @param originalInit The init function of the sibling SDK, leave blank to initialize with `@sentry/browser` */ -export function init( - passedOptions: CapacitorOptions & T, - originalInit: (passedOptions: T & BrowserOptions) => void = browserInit, +export function init( + passedOptions: CapacitorOptions, + originalInit: (passedOptions:BrowserOptions) => void = browserInit, ): void { + const finalOptions = { enableAutoSessionTracking: true, enableWatchdogTerminationTracking: true, enableCaptureFailedRequests: false, ...passedOptions, }; + finalOptions.siblingOptions && delete finalOptions.siblingOptions; + if (finalOptions.enabled === false || NATIVE.platform === 'web') { finalOptions.enableNative = false; finalOptions.enableNativeNagger = false; @@ -38,12 +41,12 @@ export function init( // const capacitorHub = new Hub(undefined, new CapacitorScope()); // makeMain(capacitorHub); const defaultIntegrations: false | Integration[] = - passedOptions.defaultIntegrations === undefined +passedOptions.defaultIntegrations === undefined ? getDefaultIntegrations(passedOptions) - : passedOptions.defaultIntegrations; +: passedOptions.defaultIntegrations; finalOptions.integrations = getIntegrationsToSetup({ - integrations: safeFactory(passedOptions.integrations, { +integrations: safeFactory(passedOptions.integrations, { loggerMessage: 'The integrations threw an error', }), defaultIntegrations, @@ -51,13 +54,13 @@ export function init( if ( finalOptions.enableNative && - !passedOptions.transport && +!passedOptions.transport && NATIVE.platform !== 'web' ) { - finalOptions.transport = passedOptions.transport || makeNativeTransport; +finalOptions.transport = passedOptions.transport || makeNativeTransport; finalOptions.transportOptions = { - ...(passedOptions.transportOptions ?? {}), +...(passedOptions.transportOptions ?? {}), bufferSize: DEFAULT_BUFFER_SIZE, }; } @@ -72,10 +75,12 @@ export function init( } const browserOptions = { + ...passedOptions.siblingOptions?.vueOptions, + ...passedOptions.siblingOptions?.nuxtClientOptions, ...finalOptions, autoSessionTracking: NATIVE.platform === 'web' && finalOptions.enableAutoSessionTracking, - } as BrowserOptions & T; + } as BrowserOptions; const mobileOptions = { ...finalOptions, @@ -83,7 +88,6 @@ export function init( NATIVE.platform !== 'web' && finalOptions.enableAutoSessionTracking, } as CapacitorClientOptions; - sdkInit(browserOptions, mobileOptions, originalInit, passedOptions.transport); } diff --git a/src/siblingOptions/index.ts b/src/siblingOptions/index.ts new file mode 100644 index 00000000..e9a2f76f --- /dev/null +++ b/src/siblingOptions/index.ts @@ -0,0 +1,3 @@ + +export type { VueOptions } from './vueOptions'; +export type { NuxtOptions } from './nuxtOptions'; diff --git a/src/siblingOptions/nuxtOptions.ts b/src/siblingOptions/nuxtOptions.ts new file mode 100644 index 00000000..e4b3bec4 --- /dev/null +++ b/src/siblingOptions/nuxtOptions.ts @@ -0,0 +1,3 @@ +import type { VueOptions } from './vueOptions'; + +export type NuxtOptions = Omit; diff --git a/src/siblingOptions/vueOptions.ts b/src/siblingOptions/vueOptions.ts new file mode 100644 index 00000000..05c2367c --- /dev/null +++ b/src/siblingOptions/vueOptions.ts @@ -0,0 +1,96 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { BrowserOptions } from '@sentry/browser'; +// FILE SYNCED WITH https://github.com/getsentry/sentry-javascript/blob/develop/packages/vue/src/types.ts + +// This is not great, but kinda necessary to make it work with Vue@2 and Vue@3 at the same time. +export interface Vue { + config: { + errorHandler?: any; + warnHandler?: any; + silent?: boolean; + }; + mixin: (mixins: Partial>) => void; +} + +export type ViewModel = { + _isVue?: boolean; + __isVue?: boolean; + $root: ViewModel; + $parent?: ViewModel; + $props: { [key: string]: any }; + $options?: { + name?: string; + propsData?: { [key: string]: any }; + _componentTag?: string; + __file?: string; + __name?: string; + }; +}; + +export interface VueOptions { + /** Vue constructor to be used inside the integration (as imported by `import Vue from 'vue'` in Vue2) */ + Vue?: Vue; + + /** + * Vue app instance(s) to be used inside the integration (as generated by `createApp` in Vue3). + */ + app?: Vue | Vue[]; + + /** + * When set to `false`, Sentry will suppress reporting of all props data + * from your Vue components for privacy concerns. + */ + attachProps: boolean; + + /** + * By default, Sentry attaches an error handler to capture exceptions and report them to Sentry. + * When `attachErrorHandler` is set to `false`, automatic error reporting is disabled. + * + * Usually, this option should stay enabled, unless you want to set up Sentry error reporting yourself. + * For example, the Sentry Nuxt SDK does not attach an error handler as it's using the error hooks provided by Nuxt. + * + * @default true + */ + attachErrorHandler: boolean; + + /** {@link TracingOptions} */ + tracingOptions?: Partial; +} + +export type Options = BrowserOptions & VueOptions; + +/** Vue specific configuration for Tracing Integration */ +export interface TracingOptions { + /** + * Decides whether to track components by hooking into its lifecycle methods. + * Can be either set to `boolean` to enable/disable tracking for all of them. + * Or to an array of specific component names (case-sensitive). + */ + trackComponents: boolean | string[]; + + /** How long to wait until the tracked root activity is marked as finished and sent of to Sentry */ + timeout: number; + + /** + * List of hooks to keep track of during component lifecycle. + * Available hooks: 'activate' | 'create' | 'destroy' | 'mount' | 'unmount' | 'update' + * Based on https://vuejs.org/v2/api/#Options-Lifecycle-Hooks + */ + hooks: Operation[]; +} + +export type Hook = + | 'activated' + | 'beforeCreate' + | 'beforeDestroy' + | 'beforeUnmount' + | 'beforeMount' + | 'beforeUpdate' + | 'created' + | 'deactivated' + | 'destroyed' + | 'unmounted' + | 'mounted' + | 'updated'; + +export type Operation = 'activate' | 'create' | 'destroy' | 'mount' | 'update' | 'unmount'; diff --git a/test/nativeOptions.test.ts b/test/nativeOptions.test.ts index 2f22dbcd..65fe7ae0 100644 --- a/test/nativeOptions.test.ts +++ b/test/nativeOptions.test.ts @@ -138,12 +138,11 @@ describe('nativeOptions', () => { test('Set logger on Android', () => { mockGetPlatform.mockReturnValue('android'); - const filteredOptions: CapacitorOptions = { - _experiments: { enableLogs : true} + const filteredOptions: CapacitorOptions & { enableLogs?: boolean } = { + enableLogs: true }; const expectedOptions = { - // @ts-ignore - enableLogs : true + enableLogs: true }; const nativeOptions = FilterNativeOptions(filteredOptions); @@ -154,8 +153,8 @@ describe('nativeOptions', () => { test('Ignore logger on iOS', () => { mockGetPlatform.mockReturnValue('ios'); - const filteredOptions: CapacitorOptions = { - _experiments: { enableLogs : true} + const filteredOptions: CapacitorOptions & { enableLogs?: boolean } = { + enableLogs: true }; const nativeOptions = FilterNativeOptions(filteredOptions); diff --git a/test/sdk.test.ts b/test/sdk.test.ts index 47f6ef84..6756969e 100644 --- a/test/sdk.test.ts +++ b/test/sdk.test.ts @@ -174,4 +174,145 @@ describe('SDK Init', () => { expect(firstFrame).toStrictEqual(expectedError); }); }); + + describe('siblingOptions', () => { + test('vueOptions are merged into browserOptions', () => { + NATIVE.platform = 'web'; + const mockApp = { config: {} } as any; + const mockOriginalInit = jest.fn(); + + init({ + dsn: 'test-dsn', + enabled: true, + siblingOptions: { + vueOptions: { + app: mockApp, + attachProps: false, + attachErrorHandler: true, + }, + }, + }, mockOriginalInit); + + // Wait for async operations + return new Promise((resolve) => { + setTimeout(() => { + expect(mockOriginalInit).toHaveBeenCalled(); + const browserOptions = mockOriginalInit.mock.calls[0][0]; + + // Verify vueOptions are merged into browserOptions + expect(browserOptions.app).toBe(mockApp); + expect(browserOptions.attachProps).toBe(false); + expect(browserOptions.attachErrorHandler).toBe(true); + + // Verify siblingOptions are not in browserOptions + expect(browserOptions.siblingOptions).toBeUndefined(); + + resolve(); + }, 10); + }); + }); + + test('nuxtClientOptions are merged into browserOptions', () => { + NATIVE.platform = 'web'; + const mockOriginalInit = jest.fn(); + + init({ + dsn: 'test-dsn', + enabled: true, + siblingOptions: { + nuxtClientOptions: { + attachProps: false, + attachErrorHandler: false, + }, + }, + }, mockOriginalInit); + + // Wait for async operations + return new Promise((resolve) => { + setTimeout(() => { + expect(mockOriginalInit).toHaveBeenCalled(); + const browserOptions = mockOriginalInit.mock.calls[0][0]; + + // Verify nuxtClientOptions are merged into browserOptions + expect(browserOptions.attachProps).toBe(false); + expect(browserOptions.attachErrorHandler).toBe(false); + + // Verify siblingOptions are not in browserOptions + expect(browserOptions.siblingOptions).toBeUndefined(); + + resolve(); + }, 10); + }); + }); + + test('nuxtClientOptions override vueOptions when both are provided', () => { + NATIVE.platform = 'web'; + const mockApp = { config: {} } as any; + const mockOriginalInit = jest.fn(); + + init({ + dsn: 'test-dsn', + enabled: true, + siblingOptions: { + vueOptions: { + app: mockApp, + attachProps: true, + attachErrorHandler: true, + }, + nuxtClientOptions: { + attachProps: false, + attachErrorHandler: false, + }, + }, + }, mockOriginalInit); + + // Wait for async operations + return new Promise((resolve) => { + setTimeout(() => { + expect(mockOriginalInit).toHaveBeenCalled(); + const browserOptions = mockOriginalInit.mock.calls[0][0]; + + // Verify nuxtClientOptions override vueOptions (merged after) + // app property should still be present from vueOptions + expect(browserOptions.app).toBe(mockApp); + // But attachProps and attachErrorHandler are overridden by nuxtClientOptions + expect(browserOptions.attachProps).toBe(false); + expect(browserOptions.attachErrorHandler).toBe(false); + + resolve(); + }, 10); + }); + }); + + test('siblingOptions are excluded from nativeOptions', () => { + NATIVE.platform = 'ios'; + const mockOriginalInit = jest.fn(); + + init({ + dsn: 'test-dsn', + enabled: true, + siblingOptions: { + vueOptions: { + attachProps: false, + attachErrorHandler: true, + }, + }, + }, mockOriginalInit); + + // Wait for async operations + return new Promise((resolve) => { + setTimeout(() => { + expect(NATIVE.initNativeSdk).toHaveBeenCalled(); + const nativeOptions = (NATIVE.initNativeSdk as jest.Mock).mock.calls[0][0]; + + // Verify siblingOptions are not in nativeOptions + expect(nativeOptions.siblingOptions).toBeUndefined(); + expect(nativeOptions.vueOptions).toBeUndefined(); + expect(nativeOptions.nuxtClientOptions).toBeUndefined(); + + resolve(); + }, 10); + }); + }); + }); });