diff --git a/.changeset/fast-lions-kick.md b/.changeset/fast-lions-kick.md new file mode 100644 index 000000000..011002780 --- /dev/null +++ b/.changeset/fast-lions-kick.md @@ -0,0 +1,5 @@ +--- +'@segment/analytics-next': patch +--- + +Add support for more argument resolver edge cases diff --git a/packages/browser/src/core/analytics/interfaces.ts b/packages/browser/src/core/analytics/interfaces.ts index d2c7e71e4..14ed074ed 100644 --- a/packages/browser/src/core/analytics/interfaces.ts +++ b/packages/browser/src/core/analytics/interfaces.ts @@ -77,24 +77,169 @@ export interface AnalyticsClassic extends AnalyticsClassicStubs { * Interface implemented by concrete Analytics class (commonly accessible if you use "await" on AnalyticsBrowser.load()) */ export interface AnalyticsCore extends CoreAnalytics { + /** + * Tracks an event. + * @param args - Event parameters. + * @example + * ```ts + * analytics.track('Event Name', { + * property1: 'value1', + * property2: 'value2' + * }); + * ``` + * @link https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/#track + * @returns A promise that resolves to a dispatched event. + */ track(...args: EventParams): Promise + + /** + * Tracks a page view. + * @param args - `[category], [name], [properties], [options], [callback]` + * @example + * ```ts + * analytics.page('My Category', 'Pricing', { + * title: 'My Overridden Title', + * myExtraProp: 'foo' + * }) + * + * analytics.page('Pricing', { + * title: 'My Overridden Title', + * myExtraProp: 'foo' + * }); + * ``` + * @link https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/#page + * @returns A promise that resolves to a dispatched event. + */ page(...args: PageParams): Promise + + /** + * Identifies a user. + * @param args - Identify parameters. + * @example + * ```ts + * analytics.identify('userId123', { + * email: 'user@example.com', + * name: 'John Doe' + * }); + * ``` + * @link https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/#identify + * @returns A promise that resolves to a dispatched event. + */ identify(...args: IdentifyParams): Promise + + /** + * Gets the group. + * @returns The group. + */ group(): Group + + /** + * Sets the group. + * @param args - Group parameters. + * @example + * ```ts + * analytics.group('groupId123', { + * name: 'Company Inc.', + * industry: 'Software' + * }); + * ``` + * @link https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/#group + * @returns A promise that resolves to a dispatched event. + */ group(...args: GroupParams): Promise + + /** + * Creates an alias for a user. + * @param args - Alias parameters. + * @example + * ```ts + * analytics.alias('newUserId123', 'oldUserId456'); + * ``` + * @link https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/#alias + * @returns A promise that resolves to a dispatched event. + */ alias(...args: AliasParams): Promise + + /** + * Tracks a screen view. + * @param args - Page parameters. + * @example + * ```ts + * analytics.screen('Home Screen', { + * title: 'Home', + * section: 'Main' + * }); + * ``` + * @link https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/#screen + * @returns A promise that resolves to a dispatched event. + */ screen(...args: PageParams): Promise + + /** + * Registers plugins. + * @param plugins - Plugins to register. + * @example + * ```ts + * analytics.register(plugin1, plugin2); + * ``` + * @link https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/#register + * @returns A promise that resolves to the context. + */ register(...plugins: Plugin[]): Promise + + /** + * Deregisters plugins. + * @param plugins - Plugin names to deregister. + * @example + * ```ts + * analytics.deregister('plugin1', 'plugin2'); + * ``` + * @link https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/#deregister + * @returns A promise that resolves to the context. + */ deregister(...plugins: string[]): Promise + + /** + * Gets the user. + * @returns The user. + */ user(): User + + /** + * The version of the analytics library. + */ readonly VERSION: string } /** * Interface implemented by AnalyticsBrowser (buffered version of analytics) (commonly accessible through AnalyticsBrowser.load()) */ -export type AnalyticsBrowserCore = Omit & { +export interface AnalyticsBrowserCore + extends Omit { + /** + * Gets the group. + * @returns A promise that resolves to the group. + */ group(): Promise + + /** + * Sets the group. + * @param args - Group parameters. + * @example + * ```ts + * analytics.group('groupId123', { + * name: 'Company Inc.', + * industry: 'Software' + * }); + * ``` + * @link https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/#group + * @returns A promise that resolves to a dispatched event. + */ group(...args: GroupParams): Promise + + /** + * Gets the user. + * @returns A promise that resolves to the user. + */ user(): Promise } diff --git a/packages/browser/src/core/arguments-resolver/__tests__/index.test.ts b/packages/browser/src/core/arguments-resolver/__tests__/index.test.ts index e6b169be0..55582da61 100644 --- a/packages/browser/src/core/arguments-resolver/__tests__/index.test.ts +++ b/packages/browser/src/core/arguments-resolver/__tests__/index.test.ts @@ -3,7 +3,7 @@ import { resolvePageArguments, resolveUserArguments, resolveAliasArguments, -} from '../' +} from '../index' import { Callback } from '../../events' import { User } from '../../user' @@ -219,6 +219,37 @@ describe(resolvePageArguments, () => { }, }, }) + expect(properties).toEqual({}) + }) + + it('should accept (category, name)', () => { + const [category, name, properties, options, cb] = resolvePageArguments( + 'category', + 'name' + ) + + expect(name).toEqual('name') + expect(category).toEqual('category') + expect(properties).toEqual({}) + expect(options).toEqual({}) + expect(cb).toEqual(undefined) + }) + + it('should accept (category, name, properties, options, cb)', () => { + const fn = jest.fn() + const [category, name, properties, options, cb] = resolvePageArguments( + 'foo', + 'name', + bananaPhone, + baseOptions, + fn + ) + + expect(category).toEqual('foo') + expect(name).toEqual('name') + expect(properties).toEqual(bananaPhone) + expect(options).toEqual(baseOptions) + expect(cb).toEqual(fn) }) test('should accept (category, name, properties, callback)', () => { @@ -254,52 +285,48 @@ describe(resolvePageArguments, () => { expect(options).toEqual({}) }) - it('should accept (name, properties, options, callback)', () => { - const fn = jest.fn() + it('should accept (name, properties)', () => { const [category, name, properties, options, cb] = resolvePageArguments( 'name', - bananaPhone, - baseOptions, - fn + bananaPhone ) - expect(category).toEqual(null) expect(name).toEqual('name') + expect(category).toEqual(null) expect(properties).toEqual(bananaPhone) - expect(options).toEqual(baseOptions) - expect(cb).toEqual(fn) + expect(options).toEqual({}) + expect(cb).toEqual(undefined) }) - it('should accept (name, properties, callback)', () => { + it('should accept (name, properties, options, callback)', () => { const fn = jest.fn() const [category, name, properties, options, cb] = resolvePageArguments( 'name', bananaPhone, + baseOptions, fn ) expect(category).toEqual(null) expect(name).toEqual('name') expect(properties).toEqual(bananaPhone) + expect(options).toEqual(baseOptions) expect(cb).toEqual(fn) - expect(options).toEqual({}) }) - it('should accept (category, name, properties, options, cb)', () => { + it('should accept (name, properties, callback)', () => { const fn = jest.fn() const [category, name, properties, options, cb] = resolvePageArguments( - 'foo', 'name', bananaPhone, - baseOptions, fn ) - expect(category).toEqual('foo') + expect(category).toEqual(null) expect(name).toEqual('name') expect(properties).toEqual(bananaPhone) - expect(options).toEqual(baseOptions) expect(cb).toEqual(fn) + expect(options).toEqual({}) }) it('should accept (name, callback)', () => { @@ -348,7 +375,7 @@ describe(resolvePageArguments, () => { expect(category).toEqual(null) }) - test('should accept (category = null, name, properties, options, callback)', () => { + test('should accept (null, name, properties, options, callback)', () => { const fn = jest.fn() const [category, name, properties, options, cb] = resolvePageArguments( null, @@ -365,6 +392,64 @@ describe(resolvePageArguments, () => { expect(cb).toEqual(fn) }) + test('should accept (name, null, properties, options, callback)', () => { + const fn = jest.fn() + const [category, name, properties, options, cb] = resolvePageArguments( + 'name', + null, + bananaPhone, + baseOptions, + fn + ) + + expect(category).toEqual(null) + expect(name).toEqual('name') + expect(properties).toEqual(bananaPhone) + expect(options).toEqual(baseOptions) + expect(cb).toEqual(fn) + }) + + test('should accept (name, null, properties)', () => { + const [category, name, properties, options] = resolvePageArguments( + 'name', + null, + bananaPhone + ) + + expect(name).toEqual('name') + expect(category).toEqual(null) + expect(properties).toEqual(bananaPhone) + expect(options).toEqual({}) + }) + + test('should accept (name, null, null, options)', () => { + const [category, name, properties, options] = resolvePageArguments( + 'name', + null, + null, + baseOptions + ) + + expect(name).toEqual('name') + expect(category).toEqual(null) + expect(properties).toEqual({}) + expect(options).toEqual(baseOptions) + }) + + test('should accept (null, null, properties)', () => { + const [category, name, properties, options, cb] = resolvePageArguments( + null, + null, + bananaPhone + ) + + expect(category).toEqual(null) + expect(name).toEqual(null) + expect(properties).toEqual(bananaPhone) + expect(options).toEqual({}) + expect(cb).toEqual(undefined) + }) + test('should accept (null, null, properties, options, callback)', () => { const fn = jest.fn() const [category, name, properties, options, cb] = resolvePageArguments( @@ -425,6 +510,16 @@ describe(resolvePageArguments, () => { expect(options).toEqual({}) expect(cb).toEqual(fn) }) + + test('should accept (null, null, null, null, callback)', () => { + const fn = jest.fn() + const [category, name, properties, options, cb] = resolvePageArguments(fn) + expect(category).toEqual(null) + expect(name).toEqual(null) + expect(properties).toEqual({}) + expect(options).toEqual({}) + expect(cb).toEqual(fn) + }) }) describe(resolveUserArguments, () => { diff --git a/packages/browser/src/core/arguments-resolver/index.ts b/packages/browser/src/core/arguments-resolver/index.ts index b5c35fe4d..71d5b26cc 100644 --- a/packages/browser/src/core/arguments-resolver/index.ts +++ b/packages/browser/src/core/arguments-resolver/index.ts @@ -51,9 +51,6 @@ export function resolveArguments( return [name, data, opts, cb] } -const isNil = (val: any): val is null | undefined => - val === null || val === undefined - /** * Helper for page, screen methods */ @@ -76,13 +73,18 @@ export function resolvePageArguments( let resolvedName: string | undefined | null = null const args = [category, name, properties, options, callback] - // handle: - // - analytics.page('name') - // - analytics.page('category', 'name') + // The legacy logic is basically: + // - If there is a string, it's the name + // - If there are two strings, it's category and name const strings = args.filter(isString) if (strings.length === 1) { - resolvedCategory = null - resolvedName = strings[0] + if (isString(args[1])) { + resolvedName = args[1] + resolvedCategory = null + } else { + resolvedName = strings[0] + resolvedCategory = null + } } else if (strings.length === 2) { if (typeof args[0] === 'string') { resolvedCategory = args[0] @@ -106,25 +108,26 @@ export function resolvePageArguments( // - analytics.page('category', 'name', properties, options, callback) // - analytics.page('category', 'name', callback) // - analytics.page(callback), etc - args.forEach((obj, argIdx) => { - if (isPlainObject(obj)) { - if (argIdx === 0) { - resolvedProperties = obj - } - - if (argIdx === 1 || argIdx == 2) { - if (isNil(resolvedProperties)) { - resolvedProperties = obj - } else { - resolvedOptions = obj - } - } - if (argIdx === 3) { - resolvedOptions = obj - } + // The legacy logic is basically: + // - If there is a plain object, it's the properties + // - If there are two plain objects, it's properties and options + const objects = args.filter(isPlainObject) + if (objects.length === 1) { + if (isPlainObject(args[2])) { + resolvedOptions = {} + resolvedProperties = args[2] + } else if (isPlainObject(args[3])) { + resolvedProperties = {} + resolvedOptions = args[3] + } else { + resolvedProperties = objects[0] + resolvedOptions = {} } - }) + } else if (objects.length === 2) { + resolvedProperties = objects[0] + resolvedOptions = objects[1] + } return [ resolvedCategory,