Skip to content

Commit 632683a

Browse files
committed
Make trait nullable and type group vs params
1 parent ec568e5 commit 632683a

File tree

16 files changed

+257
-80
lines changed

16 files changed

+257
-80
lines changed

.buildkite/pipeline.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ steps:
9999
- echo "+++ Run tests"
100100
- yarn run -T core lint
101101
- yarn run -T core test
102+
- yarn turbo run --filter=core-integration-tests lint
102103
plugins:
103104
- ssh://git@github.com/segmentio/cache-buildkite-plugin#v2.0.0:
104105
key: "v1.1-cache-dev-{{ checksum 'yarn.lock' }}"

.changeset/hip-parents-smell.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@segment/analytics-core': minor
3+
'@segment/analytics-next': minor
4+
'@segment/analytics-node': minor
5+
---
6+
7+
Make trait fields nullable. Type traits for group() differently than identify() call.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323
"release": "yarn clean && yarn build --force && changeset publish && git push origin HEAD:master --follow-tags --no-verify",
2424
"version-run-all": "yarn workspaces foreach -vpt --no-private run version",
2525
"core": "yarn workspace @segment/analytics-core",
26-
"core+deps": "turbo run --filter=@segment/analytics-core...",
26+
"core+deps": "turbo run --filter=@segment/analytics-core",
2727
"browser": "yarn workspace @segment/analytics-next",
28-
"browser+deps": "turbo run --filter=@segment/analytics-next...",
28+
"browser+deps": "turbo run --filter=@segment/analytics-next",
2929
"node": "yarn workspace @segment/analytics-node",
30-
"node+deps": "turbo run --filter=@segment/analytics-node...",
30+
"node+deps": "turbo run --filter=@segment/analytics-node",
3131
"clean": "bash scripts/clean.sh"
3232
},
3333
"packageManager": "yarn@3.2.1",

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,33 @@ export default {
119119
assertNotAny(analytics)
120120
assertIs<AnalyticsBrowser>(analytics)
121121
},
122-
'Should error if there is a type conflict in Traits': () => {
123-
const analytics = new AnalyticsBrowser().load({ writeKey: 'foo' })
124-
assertNotAny(analytics)
125-
assertIs<AnalyticsBrowser>(analytics)
126122

127-
// @ts-expect-error - id should be a string
128-
void analytics.identify('foo', { id: 123 })
123+
'Should accept traits': () => {
124+
const analytics = {} as AnalyticsBrowser
125+
126+
class Foo {
127+
name = 'hello'
128+
toJSON() {
129+
return this.name
130+
}
131+
}
132+
void analytics.identify('foo', new Foo())
133+
void analytics.identify('foo', { address: null })
134+
// @ts-expect-error - Type 'string' has no properties in common with type
135+
void analytics.identify('foo', { address: 'hello' })
136+
// @ts-expect-error - Type 'never[]' is not assignable to type
137+
void analytics.identify('foo', { id: [] })
138+
},
139+
140+
'Should accept optional ExtraContext': () => {
141+
const analytics = {} as AnalyticsBrowser
142+
void analytics.track('foo', undefined, { context: {} })
143+
void analytics.track('foo', undefined, { context: { active: true } })
144+
void analytics.track('foo', undefined, {
145+
context: {
146+
// @ts-expect-error Type 'string' is not assignable to type 'boolean | null | undefined'.ts(2322)
147+
active: 'hello',
148+
},
149+
})
129150
},
130151
}

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import {
22
AliasParams,
33
DispatchedEvent,
44
EventParams,
5+
GroupParams,
56
PageParams,
67
resolveAliasArguments,
78
resolveArguments,
89
resolvePageArguments,
9-
resolveUserArguments,
10-
UserParams,
10+
resolveIdentifyOrGroupArguments,
11+
IdentifyParams,
1112
} from '../arguments-resolver'
1213
import type { FormArgs, LinkArgs } from '../auto-track'
1314
import { isOffline } from '../connection'
@@ -202,10 +203,10 @@ export class Analytics
202203
})
203204
}
204205

205-
async identify(...args: UserParams): Promise<DispatchedEvent> {
206-
const [id, _traits, options, callback] = resolveUserArguments(this._user)(
207-
...args
208-
)
206+
async identify(...args: IdentifyParams): Promise<DispatchedEvent> {
207+
const [id, _traits, options, callback] = resolveIdentifyOrGroupArguments(
208+
this._user
209+
)(...args)
209210

210211
this._user.identify(id, _traits)
211212
const segmentEvent = this.eventFactory.identify(
@@ -227,15 +228,15 @@ export class Analytics
227228
}
228229

229230
group(): Group
230-
group(...args: UserParams): Promise<DispatchedEvent>
231-
group(...args: UserParams): Promise<DispatchedEvent> | Group {
231+
group(...args: GroupParams): Promise<DispatchedEvent>
232+
group(...args: GroupParams): Promise<DispatchedEvent> | Group {
232233
if (args.length === 0) {
233234
return this._group
234235
}
235236

236-
const [id, _traits, options, callback] = resolveUserArguments(this._group)(
237-
...args
238-
)
237+
const [id, _traits, options, callback] = resolveIdentifyOrGroupArguments(
238+
this._group
239+
)(...args)
239240

240241
this._group.identify(id, _traits)
241242
const groupId = this._group.id()

packages/browser/src/core/analytics/interfaces.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import type {
44
EventParams,
55
DispatchedEvent,
66
PageParams,
7-
UserParams,
7+
IdentifyParams,
88
AliasParams,
9+
GroupParams,
910
} from '../arguments-resolver'
1011
import type { Context } from '../context'
1112
import type { SegmentEvent } from '../events'
@@ -78,9 +79,9 @@ export interface AnalyticsClassic extends AnalyticsClassicStubs {
7879
export interface AnalyticsCore extends CoreAnalytics {
7980
track(...args: EventParams): Promise<DispatchedEvent>
8081
page(...args: PageParams): Promise<DispatchedEvent>
81-
identify(...args: UserParams): Promise<DispatchedEvent>
82+
identify(...args: IdentifyParams): Promise<DispatchedEvent>
8283
group(): Group
83-
group(...args: UserParams): Promise<DispatchedEvent>
84+
group(...args: GroupParams): Promise<DispatchedEvent>
8485
alias(...args: AliasParams): Promise<DispatchedEvent>
8586
screen(...args: PageParams): Promise<DispatchedEvent>
8687
register(...plugins: Plugin[]): Promise<Context>
@@ -94,6 +95,6 @@ export interface AnalyticsCore extends CoreAnalytics {
9495
*/
9596
export type AnalyticsBrowserCore = Omit<AnalyticsCore, 'group' | 'user'> & {
9697
group(): Promise<Group>
97-
group(...args: UserParams): Promise<DispatchedEvent>
98+
group(...args: GroupParams): Promise<DispatchedEvent>
9899
user(): Promise<User>
99100
}

packages/browser/src/core/arguments-resolver/__tests__/index.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
resolveArguments,
33
resolvePageArguments,
4-
resolveUserArguments,
4+
resolveIdentifyOrGroupArguments,
55
resolveAliasArguments,
66
} from '../'
77
import { Callback } from '../../events'
@@ -349,9 +349,9 @@ describe(resolvePageArguments, () => {
349349
})
350350
})
351351

352-
describe(resolveUserArguments, () => {
352+
describe(resolveIdentifyOrGroupArguments, () => {
353353
let user: User
354-
let resolver: ReturnType<typeof resolveUserArguments>
354+
let resolver: ReturnType<typeof resolveIdentifyOrGroupArguments>
355355
let fn: Callback
356356

357357
const userTraits = {
@@ -362,7 +362,7 @@ describe(resolveUserArguments, () => {
362362

363363
beforeEach(() => {
364364
user = new User()
365-
resolver = resolveUserArguments(user)
365+
resolver = resolveIdentifyOrGroupArguments(user)
366366
fn = jest.fn()
367367
})
368368

packages/browser/src/core/arguments-resolver/index.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import {
1212
EventProperties,
1313
SegmentEvent,
1414
Traits,
15+
GroupTraits,
16+
IdentifyTraits,
1517
} from '../events'
16-
import { ID, User } from '../user'
18+
import { Group, ID, User } from '../user'
1719

1820
/**
1921
* Helper for the track method
@@ -105,8 +107,10 @@ export function resolvePageArguments(
105107
/**
106108
* Helper for group, identify methods
107109
*/
108-
export const resolveUserArguments = (user: User): ResolveUser => {
109-
return (...args): ReturnType<ResolveUser> => {
110+
export const resolveIdentifyOrGroupArguments = <T extends Traits>(
111+
user: User | Group
112+
): ResolveIdentifyOrGroup<T> => {
113+
return (...args): ReturnType<ResolveIdentifyOrGroup<T>> => {
110114
let id: string | ID | null = null
111115
id = args.find(isString) ?? args.find(isNumber)?.toString() ?? user.id()
112116

@@ -117,7 +121,7 @@ export const resolveUserArguments = (user: User): ResolveUser => {
117121
return isPlainObject(obj) || obj === null
118122
}) as Array<Traits | null>
119123

120-
const traits = (objects[0] ?? {}) as Traits
124+
const traits = (objects[0] ?? {}) as T
121125
const opts = (objects[1] ?? {}) as Options
122126

123127
const resolvedCallback = args.find(isFunction) as Callback | undefined
@@ -146,14 +150,15 @@ export function resolveAliasArguments(
146150
return [aliasTo, aliasFrom, opts, resolvedCallback]
147151
}
148152

149-
type ResolveUser = (
153+
type ResolveIdentifyOrGroup<T extends Traits> = (
150154
id?: ID | object,
151-
traits?: Traits | Callback | null,
155+
traits?: T | Callback | null,
152156
options?: Options | Callback,
153157
callback?: Callback
154-
) => [ID, Traits, Options, Callback | undefined]
158+
) => [ID, T, Options, Callback | undefined]
155159

156-
export type UserParams = Parameters<ResolveUser>
160+
export type IdentifyParams = Parameters<ResolveIdentifyOrGroup<IdentifyTraits>>
161+
export type GroupParams = Parameters<ResolveIdentifyOrGroup<GroupTraits>>
157162
export type EventParams = Parameters<typeof resolveArguments>
158163
export type PageParams = Parameters<typeof resolvePageArguments>
159164
export type AliasParams = Parameters<typeof resolveAliasArguments>

packages/browser/src/core/events/interfaces.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,28 @@ import {
1111
JSONValue,
1212
JSONPrimitive,
1313
JSONObject,
14+
CoreGroupTraits,
15+
CoreIndentifyTraits,
1416
} from '@segment/analytics-core'
1517

1618
export interface Options extends CoreOptions {}
1719

18-
// This is not ideal, but it works with all the edge cases
1920
export interface Traits extends CoreAnalyticsTraits {}
2021

22+
/**
23+
* Traits are pieces of information you know about a group.
24+
* This interface represents reserved traits that Segment has standardized.
25+
* @link https://segment.com/docs/connections/spec/group/#traits
26+
*/
27+
export interface GroupTraits extends CoreGroupTraits {}
28+
29+
/**
30+
* Traits are pieces of information you know about a user.
31+
* This interface represents reserved traits that Segment has standardized.
32+
* @link https://segment.com/docs/connections/spec/identify/#traits
33+
*/
34+
export interface IdentifyTraits extends CoreIndentifyTraits {}
35+
2136
export type EventProperties = Record<string, any>
2237

2338
export interface SegmentEvent extends CoreSegmentEvent {}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { CoreAnalyticsTraits, CoreExtraContext } from '@segment/analytics-core'
2+
3+
class TestClass {
4+
name = 'hello'
5+
toJSON() {
6+
return this.name
7+
}
8+
}
9+
10+
export default {
11+
'CoreAnalyticsTraits test': () => {
12+
let traits: CoreAnalyticsTraits = {}
13+
14+
// should accept a class
15+
traits = new TestClass()
16+
// should be nullable
17+
traits = { address: null }
18+
// should accept aribtrary properties
19+
traits = { address: {}, foo: 123 }
20+
// should fail with type conflicts
21+
// @ts-expect-error
22+
traits = { address: 'hello' }
23+
},
24+
25+
'CoreExtraContext test': () => {
26+
// should accept a class
27+
let ec: CoreExtraContext = {}
28+
ec = new TestClass()
29+
// should error if type conflict
30+
// @ts-expect-error
31+
ec = { page: { path: {} } }
32+
},
33+
}

0 commit comments

Comments
 (0)