Skip to content

Commit 17519a5

Browse files
refactor(react-native): bypass proxy once client is initialized
1 parent e650753 commit 17519a5

File tree

3 files changed

+62
-44
lines changed

3 files changed

+62
-44
lines changed

packages/platforms/react-native/lib/client.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,28 @@ export function registerClient<C extends ReactNativeConfiguration = ReactNativeC
1919
const clock = createClock(performance)
2020
const appStartTime = clock.now()
2121

22-
// Proxy wrapper that provides lazy initialization of the BugsnagPerformance client.
23-
// How it works:
24-
// 1. If registerClient() was called (e.g., by Expo), the proxy delegates to that pre-configured instance
25-
// 2. Otherwise, on first property access, it creates a default client via createReactNativeClient()
26-
// 3. All subsequent accesses use the same singleton instance, ensuring one client per app
27-
// The Proxy intercepts all property accesses and forwards them to the actual client instance.
22+
// Proxy wrapper for lazy initialization of the BugsnagPerformance client.
23+
// On first property access:
24+
// - Uses pre-configured client from registerClient() if available, otherwise creates a new client
25+
// - Copies all client properties to the proxy target and removes the get trap
26+
// - All subsequent accesses go directly to the target, bypassing the proxy for optimal performance
2827
const client = new Proxy({} as BugsnagPerformance<ReactNativeConfiguration, PlatformExtensions>, {
29-
get (_target, prop) {
28+
get (target, prop) {
3029
// React uses the $$typeof property to identify React elements - we return undefined
3130
// to avoid triggering client creation during React's internal type checking
3231
if (prop === '$$typeof') return undefined
3332

33+
// initialize the client instance here if it hasn't already been set via registerClient()
3434
if (!clientInstance) {
3535
clientInstance = createReactNativeClient({ appStartTime, clock })
3636
}
37+
38+
// Now that the client has been initialized, we can add all its properties to the proxy target
39+
// and remove the get trap in order to bypass the proxy for future accesses
3740
const client = clientInstance as BugsnagPerformance<ReactNativeConfiguration, PlatformExtensions>
41+
Object.defineProperties(target, Object.getOwnPropertyDescriptors(client))
42+
delete this.get
43+
3844
return client[prop as keyof typeof client]
3945
}
4046
})

packages/platforms/react-native/tests/client.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type { ReactNativeConfiguration } from '../lib/config'
33
import type { BugsnagPerformance } from '@bugsnag/core-performance'
44
import type { PlatformExtensions } from '../lib/platform-extensions'
5+
import { IncrementingClock } from '@bugsnag/js-performance-test-utilities'
56

67
let client: BugsnagPerformance<ReactNativeConfiguration, PlatformExtensions>
78

@@ -20,3 +21,51 @@ describe('Default React Native Client', () => {
2021
expect(client.attach).toBeDefined()
2122
})
2223
})
24+
25+
describe('singleton behavior', () => {
26+
beforeEach(() => {
27+
// Reset modules to clear the singleton
28+
jest.resetModules()
29+
})
30+
31+
it('should allow custom clients to be set and used as default', () => {
32+
const { registerClient } = require('../lib/client')
33+
const { createReactNativeClient } = require('../lib/create-client')
34+
const clock = new IncrementingClock()
35+
36+
// Import the default client
37+
const defaultClient = require('../lib/client').default
38+
39+
// Create and register a custom client
40+
const testClient = createReactNativeClient({ clock })
41+
registerClient(testClient)
42+
43+
// The default should proxy to the test client
44+
expect(defaultClient.start).toBe(testClient.start)
45+
expect(defaultClient.startSpan).toBe(testClient.startSpan)
46+
})
47+
48+
it('should create default client if none is set', () => {
49+
const createReactNativeClient = jest.spyOn(require('../lib/create-client'), 'createReactNativeClient')
50+
const client = require('../lib/client')
51+
const defaultClient = client.default
52+
53+
// createReactNativeClient should be called on first access
54+
expect(createReactNativeClient).not.toHaveBeenCalled()
55+
expect(defaultClient.start).toBeDefined()
56+
expect(defaultClient.startSpan).toBeDefined()
57+
expect(createReactNativeClient).toHaveBeenCalledTimes(1)
58+
})
59+
60+
it('should correctly handle getter properties after trap removal', () => {
61+
const defaultClient = require('../lib/client').default
62+
63+
// starting a span will trigger trap removal
64+
const span = defaultClient.startSpan('test-span')
65+
66+
// Getters such as currentSpanContext should return the current value
67+
// as opposed to the value at the time of first access
68+
expect(defaultClient.currentSpanContext).toBeDefined()
69+
expect(defaultClient.currentSpanContext.id).toBe(span.id)
70+
})
71+
})

packages/platforms/react-native/tests/create-client.test.ts

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable @typescript-eslint/no-var-requires */
21
import {
32
InMemoryDelivery,
43
IncrementingClock,
@@ -145,40 +144,4 @@ describe('createReactNativeClient', () => {
145144
expect(client.attach).toBeUndefined()
146145
})
147146
})
148-
149-
describe('singleton behavior', () => {
150-
beforeEach(() => {
151-
// Reset modules to clear the singleton
152-
jest.resetModules()
153-
})
154-
155-
it('should allow custom clients to be set and used as default', () => {
156-
const { registerClient } = require('../lib/client')
157-
const { createReactNativeClient } = require('../lib/create-client')
158-
const clock = new IncrementingClock()
159-
160-
// Import the default client
161-
const defaultClient = require('../lib/client').default
162-
163-
// Create and register a custom client
164-
const testClient = createReactNativeClient({ clock })
165-
registerClient(testClient)
166-
167-
// The default should proxy to the test client
168-
expect(defaultClient.start).toBe(testClient.start)
169-
expect(defaultClient.startSpan).toBe(testClient.startSpan)
170-
})
171-
172-
it('should create default client if none is set', () => {
173-
const createReactNativeClient = jest.spyOn(require('../lib/create-client'), 'createReactNativeClient')
174-
const client = require('../lib/client')
175-
const defaultClient = client.default
176-
177-
// createReactNativeClient should be called on first access
178-
expect(createReactNativeClient).not.toHaveBeenCalled()
179-
expect(defaultClient.start).toBeDefined()
180-
expect(defaultClient.startSpan).toBeDefined()
181-
expect(createReactNativeClient).toHaveBeenCalledTimes(1)
182-
})
183-
})
184147
})

0 commit comments

Comments
 (0)