Skip to content

Commit 5cd9358

Browse files
authored
Do not allow the user method to change its return types over its lifecycle (#567)
add unit tests
1 parent d9481e6 commit 5cd9358

File tree

6 files changed

+94
-13
lines changed

6 files changed

+94
-13
lines changed

.changeset/slimy-worms-fail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@segment/analytics-next': patch
3+
---
4+
5+
Do not allow the "user" method to change its return types over its lifecycle. We should always return a promise for wrapped methods in AnalyticsBrowser, regardless if the underlying Analytics method is sync or async.

packages/browser/src/browser/__tests__/analytics-pre-init.integration.test.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Context } from '../../core/context'
55
import * as Factory from '../../test-helpers/factories'
66
import { sleep } from '../../test-helpers/sleep'
77
import { setGlobalCDNUrl } from '../../lib/parse-cdn'
8+
import { User } from '../../core/user'
89

910
jest.mock('unfetch')
1011

@@ -59,19 +60,38 @@ describe('Pre-initialization', () => {
5960
expect(trackSpy).toBeCalledTimes(1)
6061
})
6162

62-
test('"return types should not change over the lifecycle for ordinary methods', async () => {
63+
test('"return types should not change over the lifecycle for async methods', async () => {
6364
const ajsBrowser = AnalyticsBrowser.load({ writeKey })
6465

6566
const trackCtxPromise1 = ajsBrowser.track('foo', { name: 'john' })
6667
expect(trackCtxPromise1).toBeInstanceOf(Promise)
67-
const ctx1 = await trackCtxPromise1
68-
expect(ctx1).toBeInstanceOf(Context)
68+
await ajsBrowser
6969

7070
// loaded
7171
const trackCtxPromise2 = ajsBrowser.track('foo', { name: 'john' })
7272
expect(trackCtxPromise2).toBeInstanceOf(Promise)
73-
const ctx2 = await trackCtxPromise2
74-
expect(ctx2).toBeInstanceOf(Context)
73+
74+
expect(await trackCtxPromise1).toBeInstanceOf(Context)
75+
expect(await trackCtxPromise2).toBeInstanceOf(Context)
76+
})
77+
78+
test('return types should not change over lifecycle for sync methods', async () => {
79+
const ajsBrowser = AnalyticsBrowser.load({ writeKey })
80+
const user = ajsBrowser.user()
81+
expect(user).toBeInstanceOf(Promise)
82+
await ajsBrowser
83+
84+
// loaded
85+
const user2 = ajsBrowser.user()
86+
expect(user2).toBeInstanceOf(Promise)
87+
88+
expect(await user).toBeInstanceOf(User)
89+
expect(await user2).toBeInstanceOf(User)
90+
})
91+
92+
test('version should return version', async () => {
93+
const ajsBrowser = AnalyticsBrowser.load({ writeKey })
94+
expect(typeof ajsBrowser.VERSION).toBe('string')
7595
})
7696

7797
test('If a user sends multiple events, all of those event gets flushed', async () => {

packages/browser/src/browser/__tests__/typedef-tests/analytics-browser.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Analytics } from '@/core/analytics'
22
import { Context } from '@/core/context'
33
import { AnalyticsBrowser } from '@/browser'
44
import { assertNotAny, assertIs } from '@/test-helpers/type-assertions'
5-
import { Group } from '../../../core/user'
5+
import { Group, User } from '../../../core/user'
66

77
/**
88
* These are general typescript definition tests;
@@ -67,4 +67,11 @@ export default {
6767
assertIs<Promise<Context>>(grpResult)
6868
}
6969
},
70+
'User should have the correct type': () => {
71+
const ajs = AnalyticsBrowser.load({ writeKey: 'foo' })
72+
{
73+
const grpResult = ajs.user()
74+
assertIs<Promise<User>>(grpResult)
75+
}
76+
},
7077
}

packages/browser/src/core/buffer/__tests__/index.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { Analytics } from '../../analytics'
99
import { Context } from '../../context'
1010
import { sleep } from '@/test-helpers/sleep'
11+
import { User } from '../../user'
1112

1213
describe('PreInitMethodCallBuffer', () => {
1314
describe('push', () => {
@@ -99,6 +100,57 @@ describe('AnalyticsBuffered', () => {
99100
expect(context).toEqual(ctx)
100101
})
101102

103+
it('should wrap async methods in a promise', async () => {
104+
const ajs = new Analytics({ writeKey: 'foo' })
105+
const ctx = new Context({ type: 'track' })
106+
107+
ajs.track = () => Promise.resolve(ctx)
108+
109+
const buffered = new AnalyticsBuffered((buffer) => {
110+
return new Promise((resolve) =>
111+
setTimeout(() => {
112+
flushAnalyticsCallsInNewTask(ajs, buffer)
113+
resolve([ajs, ctx])
114+
}, 0)
115+
)
116+
})
117+
const result = buffered.track('foo')
118+
expect(result).toBeInstanceOf(Promise)
119+
120+
await buffered
121+
122+
const result2 = buffered.track('bar')
123+
expect(result2).toBeInstanceOf(Promise)
124+
125+
expect(await result).toBeInstanceOf(Context)
126+
expect(await result2).toBeInstanceOf(Context)
127+
})
128+
129+
it('should wrap synchronous methods in a promise', async () => {
130+
const ajs = new Analytics({ writeKey: 'foo' })
131+
ajs.user = () => new User()
132+
const ctx = new Context({ type: 'track' })
133+
134+
const buffered = new AnalyticsBuffered((buffer) => {
135+
return new Promise((resolve) =>
136+
setTimeout(() => {
137+
flushAnalyticsCallsInNewTask(ajs, buffer)
138+
resolve([ajs, ctx])
139+
}, 0)
140+
)
141+
})
142+
const result = buffered.user()
143+
expect(result).toBeInstanceOf(Promise)
144+
145+
await buffered
146+
147+
const result2 = buffered.user()
148+
expect(result2).toBeInstanceOf(Promise)
149+
150+
expect(await result).toBeInstanceOf(User)
151+
expect(await result2).toBeInstanceOf(User)
152+
})
153+
102154
describe('the "this" value of proxied analytics methods', () => {
103155
test('should be the ajs instance for non-chainable methods (that return a promise)', async () => {
104156
const ajs = new Analytics({ writeKey: 'foo' })

packages/browser/src/core/buffer/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ export class AnalyticsBuffered
248248
...args: Parameters<Analytics[T]>
249249
): Promise<ReturnTypeUnwrap<Analytics[T]>> => {
250250
if (this.instance) {
251-
return (this.instance[methodName] as Function)(...args)
251+
const result = (this.instance[methodName] as Function)(...args)
252+
return Promise.resolve(result)
252253
}
253254

254255
return new Promise((resolve, reject) => {

packages/browser/src/test-helpers/type-assertions.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ type NotUnknown<T> = unknown extends T ? never : T
55
type NotTopType<T> = NotAny<T> & NotUnknown<T>
66

77
// this is not meant to be run, just for type tests
8-
export function assertNotAny<T>(val: NotTopType<T>) {
9-
console.log(val)
10-
}
8+
export function assertNotAny<T>(_val: NotTopType<T>) {}
119

1210
// this is not meant to be run, just for type tests
13-
export function assertIs<T extends SomeType, SomeType = any>(val: T) {
14-
console.log(val)
15-
}
11+
export function assertIs<T extends SomeType, SomeType = any>(_val: T) {}

0 commit comments

Comments
 (0)