Skip to content

Commit 2f56ec0

Browse files
Merge pull request #6462 from Shopify/fix-observe-user-stable
[stable] Set the user ID for Observe
2 parents 8deeb29 + 5cf22fa commit 2f56ec0

File tree

3 files changed

+133
-5
lines changed

3 files changed

+133
-5
lines changed

packages/cli-kit/src/private/node/session.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,82 @@ describe('getLastSeenUserIdAfterAuth', () => {
460460
})
461461
})
462462

463+
describe('setLastSeenUserIdAfterAuth', () => {
464+
beforeEach(() => {
465+
// Reset the userId before each test
466+
setLastSeenUserIdAfterAuth(undefined as any)
467+
})
468+
469+
test('sets the userId correctly', async () => {
470+
// Given
471+
const testUserId = 'test-user-id-123'
472+
473+
// When
474+
setLastSeenUserIdAfterAuth(testUserId)
475+
476+
// Then
477+
const retrievedUserId = await getLastSeenUserIdAfterAuth()
478+
expect(retrievedUserId).toBe(testUserId)
479+
})
480+
481+
test('overwrites existing userId when called multiple times', async () => {
482+
// Given
483+
const firstUserId = 'first-user-id'
484+
const secondUserId = 'second-user-id'
485+
486+
// When
487+
setLastSeenUserIdAfterAuth(firstUserId)
488+
let retrievedUserId = await getLastSeenUserIdAfterAuth()
489+
expect(retrievedUserId).toBe(firstUserId)
490+
491+
setLastSeenUserIdAfterAuth(secondUserId)
492+
retrievedUserId = await getLastSeenUserIdAfterAuth()
493+
494+
// Then
495+
expect(retrievedUserId).toBe(secondUserId)
496+
})
497+
498+
test('handles empty string userId', async () => {
499+
// Given
500+
const emptyUserId = ''
501+
502+
// When
503+
setLastSeenUserIdAfterAuth(emptyUserId)
504+
505+
// Then
506+
const retrievedUserId = await getLastSeenUserIdAfterAuth()
507+
// Empty string is falsy, so it falls back to 'unknown'
508+
expect(retrievedUserId).toBe('unknown')
509+
})
510+
511+
test('handles undefined userId', async () => {
512+
// Given & When
513+
setLastSeenUserIdAfterAuth(undefined as any)
514+
515+
// Then
516+
const retrievedUserId = await getLastSeenUserIdAfterAuth()
517+
// Undefined is falsy, so it falls back to 'unknown'
518+
expect(retrievedUserId).toBe('unknown')
519+
})
520+
521+
test('persists userId across multiple getLastSeenUserIdAfterAuth calls', async () => {
522+
// Given
523+
const testUserId = 'persistent-user-id'
524+
setLastSeenUserIdAfterAuth(testUserId)
525+
526+
// When - Call getLastSeenUserIdAfterAuth multiple times
527+
const firstCall = await getLastSeenUserIdAfterAuth()
528+
const secondCall = await getLastSeenUserIdAfterAuth()
529+
const thirdCall = await getLastSeenUserIdAfterAuth()
530+
531+
// Then - All calls should return the same userId and fetchSessions should not be called
532+
expect(firstCall).toBe(testUserId)
533+
expect(secondCall).toBe(testUserId)
534+
expect(thirdCall).toBe(testUserId)
535+
expect(fetchSessions).not.toHaveBeenCalled()
536+
})
537+
})
538+
463539
describe('getLastSeenAuthMethod', () => {
464540
beforeEach(() => {
465541
vi.mocked(getCurrentSessionId).mockReturnValue(undefined)

packages/cli-kit/src/public/node/error-handler.test.ts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,30 @@ import {mockAndCaptureOutput} from './testing/output.js'
55
import * as error from './error.js'
66
import {hashString} from '../../public/node/crypto.js'
77
import {isLocalEnvironment} from '../../private/node/context/service.js'
8+
import {getLastSeenUserIdAfterAuth} from '../../private/node/session.js'
89
import {settings} from '@oclif/core'
910
import {beforeEach, describe, expect, test, vi} from 'vitest'
1011

1112
const onNotify = vi.fn()
13+
const capturedEventHandler = vi.fn()
1214
let lastBugsnagEvent: {addMetadata: ReturnType<typeof vi.fn>} | undefined
1315

1416
vi.mock('process')
1517
vi.mock('@bugsnag/js', () => {
1618
return {
1719
default: {
18-
notify: (reportedError: any, args: any, callback: any) => {
20+
notify: (reportedError: any, eventHandler: any, callback: any) => {
1921
onNotify(reportedError)
20-
if (typeof args === 'function') {
21-
const event = {addMetadata: vi.fn()}
22-
lastBugsnagEvent = event as any
23-
args(event)
22+
// Create a mock event to pass to the event handler
23+
const mockEvent = {
24+
severity: '',
25+
unhandled: false,
26+
setUser: vi.fn(),
27+
addMetadata: vi.fn(),
2428
}
29+
eventHandler(mockEvent)
30+
capturedEventHandler(mockEvent)
31+
lastBugsnagEvent = mockEvent as any
2532
callback(null)
2633
},
2734
isStarted: () => true,
@@ -33,6 +40,7 @@ vi.mock('./cli.js')
3340
vi.mock('./context/local.js')
3441
vi.mock('../../public/node/crypto.js')
3542
vi.mock('../../private/node/context/service.js')
43+
vi.mock('../../private/node/session.js')
3644
vi.mock('@oclif/core', () => ({
3745
settings: {
3846
debug: false,
@@ -47,9 +55,11 @@ beforeEach(() => {
4755
vi.mocked(hashString).mockReturnValue('hashed-macaddress')
4856
vi.mocked(isUnitTest).mockReturnValue(true)
4957
onNotify.mockClear()
58+
capturedEventHandler.mockClear()
5059
lastBugsnagEvent = undefined
5160
vi.mocked(settings).debug = false
5261
vi.mocked(isLocalEnvironment).mockReturnValue(false)
62+
vi.mocked(getLastSeenUserIdAfterAuth).mockResolvedValue('test-user-id-123')
5363
})
5464

5565
describe('errorHandler', async () => {
@@ -168,6 +178,8 @@ describe('skips sending errors to Bugsnag', () => {
168178
describe('sends errors to Bugsnag', () => {
169179
test('processes Error instances as unhandled', async () => {
170180
const toThrow = new Error('In test')
181+
capturedEventHandler.mockClear()
182+
171183
const res = await sendErrorToBugsnag(toThrow, 'unexpected_error')
172184
expect(res.reported).toEqual(true)
173185
expect(res.unhandled).toEqual(true)
@@ -217,6 +229,43 @@ describe('sends errors to Bugsnag', () => {
217229
expect(mockOutput.debug()).toMatch('Error reporting to Bugsnag: Error: Bugsnag is down')
218230
})
219231

232+
test('sets user ID from getLastSeenUserIdAfterAuth when reporting to Bugsnag', async () => {
233+
// Given
234+
capturedEventHandler.mockClear()
235+
const testUserId = 'specific-test-user-id'
236+
vi.mocked(getLastSeenUserIdAfterAuth).mockResolvedValue(testUserId)
237+
const toThrow = new Error('In test')
238+
239+
// When
240+
const res = await sendErrorToBugsnag(toThrow, 'unexpected_error')
241+
242+
// Then
243+
expect(res.reported).toEqual(true)
244+
expect(capturedEventHandler).toHaveBeenCalled()
245+
246+
const mockEvent = capturedEventHandler.mock.calls[0]![0]
247+
expect(mockEvent.setUser).toHaveBeenCalledWith(testUserId)
248+
expect(mockEvent.severity).toEqual('error')
249+
expect(mockEvent.unhandled).toEqual(true)
250+
})
251+
252+
test('handles missing user ID gracefully', async () => {
253+
// Given
254+
capturedEventHandler.mockClear()
255+
vi.mocked(getLastSeenUserIdAfterAuth).mockResolvedValue('unknown')
256+
const toThrow = new Error('In test')
257+
258+
// When
259+
const res = await sendErrorToBugsnag(toThrow, 'unexpected_error')
260+
261+
// Then
262+
expect(res.reported).toEqual(true)
263+
expect(capturedEventHandler).toHaveBeenCalled()
264+
265+
const mockEvent = capturedEventHandler.mock.calls[0]![0]
266+
expect(mockEvent.setUser).toHaveBeenCalledWith('unknown')
267+
})
268+
220269
test('attaches custom metadata with allowed slice_name when startCommand is present', async () => {
221270
await metadata.addSensitiveMetadata(() => ({
222271
commandStartOptions: {startTime: Date.now(), startCommand: 'app dev', startArgs: []},

packages/cli-kit/src/public/node/error-handler.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {outputDebug, outputInfo} from '../../public/node/output.js'
1616
import {bugsnagApiKey, reportingRateLimit} from '../../private/node/constants.js'
1717
import {CLI_KIT_VERSION} from '../common/version.js'
1818
import {runWithRateLimit} from '../../private/node/conf-store.js'
19+
import {getLastSeenUserIdAfterAuth} from '../../private/node/session.js'
1920
import {settings, Interfaces} from '@oclif/core'
2021
import StackTracey from 'stacktracey'
2122
import Bugsnag, {Event} from '@bugsnag/js'
@@ -125,11 +126,13 @@ export async function sendErrorToBugsnag(
125126

126127
if (report) {
127128
initializeBugsnag()
129+
const userId = await getLastSeenUserIdAfterAuth()
128130
await new Promise((resolve, reject) => {
129131
outputDebug(`Reporting ${unhandled ? 'unhandled' : 'handled'} error to Bugsnag: ${reportableError.message}`)
130132
const eventHandler = (event: Event) => {
131133
event.severity = 'error'
132134
event.unhandled = unhandled
135+
event.setUser(userId)
133136
// Attach command metadata so we know which CLI command triggered the error
134137
const {commandStartOptions} = metadata.getAllSensitiveMetadata()
135138
const {startCommand} = commandStartOptions ?? {}

0 commit comments

Comments
 (0)