Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ steps:
- echo "+++ Run tests"
- yarn run -T core lint
- yarn run -T core test
- yarn turbo run --filter=core-integration-tests lint
plugins:
- ssh://git@github.com/segmentio/cache-buildkite-plugin#v2.0.0:
key: "v1.1-cache-dev-{{ checksum 'yarn.lock' }}"
Expand Down
7 changes: 7 additions & 0 deletions .changeset/hip-parents-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@segment/analytics-core': minor
'@segment/analytics-next': minor
'@segment/analytics-node': minor
---

Make trait fields nullable. Type traits for group() differently than identify() call.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
"release": "yarn clean && yarn build --force && changeset publish && git push origin HEAD:master --follow-tags --no-verify",
"version-run-all": "yarn workspaces foreach -vpt --no-private run version",
"core": "yarn workspace @segment/analytics-core",
"core+deps": "turbo run --filter=@segment/analytics-core...",
"core+deps": "turbo run --filter=@segment/analytics-core",
"browser": "yarn workspace @segment/analytics-next",
"browser+deps": "turbo run --filter=@segment/analytics-next...",
"browser+deps": "turbo run --filter=@segment/analytics-next",
"node": "yarn workspace @segment/analytics-node",
"node+deps": "turbo run --filter=@segment/analytics-node...",
"node+deps": "turbo run --filter=@segment/analytics-node",
"clean": "bash scripts/clean.sh"
},
"packageManager": "yarn@3.2.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,33 @@ export default {
assertNotAny(analytics)
assertIs<AnalyticsBrowser>(analytics)
},
'Should error if there is a type conflict in Traits': () => {
const analytics = new AnalyticsBrowser().load({ writeKey: 'foo' })
assertNotAny(analytics)
assertIs<AnalyticsBrowser>(analytics)

// @ts-expect-error - id should be a string
void analytics.identify('foo', { id: 123 })
'Should accept traits': () => {
const analytics = {} as AnalyticsBrowser

class Foo {
name = 'hello'
toJSON() {
return this.name
}
}
void analytics.identify('foo', new Foo())
void analytics.identify('foo', { address: null })
// @ts-expect-error - Type 'string' has no properties in common with type
void analytics.identify('foo', { address: 'hello' })
// @ts-expect-error - Type 'never[]' is not assignable to type
void analytics.identify('foo', { id: [] })
},

'Should accept optional ExtraContext': () => {
const analytics = {} as AnalyticsBrowser
void analytics.track('foo', undefined, { context: {} })
void analytics.track('foo', undefined, { context: { active: true } })
void analytics.track('foo', undefined, {
context: {
// @ts-expect-error Type 'string' is not assignable to type 'boolean | null | undefined'.ts(2322)
active: 'hello',
},
})
},
}
9 changes: 5 additions & 4 deletions packages/browser/src/core/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import {
AliasParams,
DispatchedEvent,
EventParams,
GroupParams,
PageParams,
resolveAliasArguments,
resolveArguments,
resolvePageArguments,
resolveUserArguments,
UserParams,
IdentifyParams,
} from '../arguments-resolver'
import type { FormArgs, LinkArgs } from '../auto-track'
import { isOffline } from '../connection'
Expand Down Expand Up @@ -202,7 +203,7 @@ export class Analytics
})
}

async identify(...args: UserParams): Promise<DispatchedEvent> {
async identify(...args: IdentifyParams): Promise<DispatchedEvent> {
const [id, _traits, options, callback] = resolveUserArguments(this._user)(
...args
)
Expand All @@ -227,8 +228,8 @@ export class Analytics
}

group(): Group
group(...args: UserParams): Promise<DispatchedEvent>
group(...args: UserParams): Promise<DispatchedEvent> | Group {
group(...args: GroupParams): Promise<DispatchedEvent>
group(...args: GroupParams): Promise<DispatchedEvent> | Group {
if (args.length === 0) {
return this._group
}
Expand Down
9 changes: 5 additions & 4 deletions packages/browser/src/core/analytics/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import type {
EventParams,
DispatchedEvent,
PageParams,
UserParams,
IdentifyParams,
AliasParams,
GroupParams,
} from '../arguments-resolver'
import type { Context } from '../context'
import type { SegmentEvent } from '../events'
Expand Down Expand Up @@ -78,9 +79,9 @@ export interface AnalyticsClassic extends AnalyticsClassicStubs {
export interface AnalyticsCore extends CoreAnalytics {
track(...args: EventParams): Promise<DispatchedEvent>
page(...args: PageParams): Promise<DispatchedEvent>
identify(...args: UserParams): Promise<DispatchedEvent>
identify(...args: IdentifyParams): Promise<DispatchedEvent>
group(): Group
group(...args: UserParams): Promise<DispatchedEvent>
group(...args: GroupParams): Promise<DispatchedEvent>
alias(...args: AliasParams): Promise<DispatchedEvent>
screen(...args: PageParams): Promise<DispatchedEvent>
register(...plugins: Plugin[]): Promise<Context>
Expand All @@ -94,6 +95,6 @@ export interface AnalyticsCore extends CoreAnalytics {
*/
export type AnalyticsBrowserCore = Omit<AnalyticsCore, 'group' | 'user'> & {
group(): Promise<Group>
group(...args: UserParams): Promise<DispatchedEvent>
group(...args: GroupParams): Promise<DispatchedEvent>
user(): Promise<User>
}
19 changes: 12 additions & 7 deletions packages/browser/src/core/arguments-resolver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
EventProperties,
SegmentEvent,
Traits,
GroupTraits,
UserTraits,
} from '../events'
import { ID, User } from '../user'

Expand Down Expand Up @@ -105,8 +107,10 @@ export function resolvePageArguments(
/**
* Helper for group, identify methods
*/
export const resolveUserArguments = (user: User): ResolveUser => {
return (...args): ReturnType<ResolveUser> => {
export const resolveUserArguments = <T extends Traits, U extends User>(
user: U
): ResolveUser<T> => {
return (...args): ReturnType<ResolveUser<T>> => {
let id: string | ID | null = null
id = args.find(isString) ?? args.find(isNumber)?.toString() ?? user.id()

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

const traits = (objects[0] ?? {}) as Traits
const traits = (objects[0] ?? {}) as T
const opts = (objects[1] ?? {}) as Options

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

type ResolveUser = (
type ResolveUser<T extends Traits> = (
id?: ID | object,
traits?: Traits | Callback | null,
traits?: T | Callback | null,
options?: Options | Callback,
callback?: Callback
) => [ID, Traits, Options, Callback | undefined]
) => [ID, T, Options, Callback | undefined]

export type UserParams = Parameters<ResolveUser>
export type IdentifyParams = Parameters<ResolveUser<UserTraits>>
export type GroupParams = Parameters<ResolveUser<GroupTraits>>
export type EventParams = Parameters<typeof resolveArguments>
export type PageParams = Parameters<typeof resolvePageArguments>
export type AliasParams = Parameters<typeof resolveAliasArguments>
Expand Down
7 changes: 4 additions & 3 deletions packages/browser/src/core/events/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
CoreAnalyticsTraits,
CoreOptions,
CoreSegmentEvent,
Callback,
Expand All @@ -11,12 +10,14 @@ import {
JSONValue,
JSONPrimitive,
JSONObject,
GroupTraits,
UserTraits,
Traits,
} from '@segment/analytics-core'

export interface Options extends CoreOptions {}

// This is not ideal, but it works with all the edge cases
export interface Traits extends CoreAnalyticsTraits {}
export type { GroupTraits, UserTraits, Traits }

export type EventProperties = Record<string, any>

Expand Down
33 changes: 33 additions & 0 deletions packages/core-integration-tests/src/typedef-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Traits, CoreExtraContext } from '@segment/analytics-core'

class TestClass {
name = 'hello'
toJSON() {
return this.name
}
}

export default {
'Traits test': () => {
let traits: Traits = {}

// should accept a class
traits = new TestClass()
// should be nullable
traits = { address: null }
// should accept aribtrary properties
traits = { address: {}, foo: 123 }
// should fail with type conflicts
// @ts-expect-error
traits = { address: 'hello' }
},

'CoreExtraContext test': () => {
// should accept a class
let ec: CoreExtraContext = {}
ec = new TestClass()
// should error if type conflict
// @ts-expect-error
ec = { page: { path: {} } }
},
}
7 changes: 4 additions & 3 deletions packages/core/src/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { ID, User } from '../user'
import {
Integrations,
EventProperties,
CoreAnalyticsTraits,
CoreSegmentEvent,
CoreOptions,
CoreExtraContext,
UserTraits,
GroupTraits,
} from './interfaces'
import { pickBy } from '../utils/pick'
import { validateEvent } from '../validation/assertions'
Expand Down Expand Up @@ -103,7 +104,7 @@ export class EventFactory {

identify(
userId: ID,
traits?: CoreAnalyticsTraits,
traits?: UserTraits,
options?: CoreOptions,
globalIntegrations?: Integrations
): CoreSegmentEvent {
Expand All @@ -119,7 +120,7 @@ export class EventFactory {

group(
groupId: ID,
traits?: CoreAnalyticsTraits,
traits?: GroupTraits,
options?: CoreOptions,
globalIntegrations?: Integrations
): CoreSegmentEvent {
Expand Down
Loading