From 48cb62acdbcd3891feda7ac265324a8164be2288 Mon Sep 17 00:00:00 2001 From: Eric West Date: Thu, 9 Jul 2026 14:49:52 -0700 Subject: [PATCH 1/2] feat: add extras support to user.identify Add optional extras.createdAt and extras.updateOnly to user.identify, mirroring the events/track extras pattern. The SDK maps camelCase fields to API snake_case (created_at, update_only) and defaults created_at to the current time when omitted. - Add sendUserIdentifyRequest() parallel to sendEventTrackRequest() - Extend UserIdentifyRequest with extras type definitions - Add Node and browser tests for default and explicit extras Related docs: getvero/mint-docs (feat/add-extras-to-identify) Related API: getvero/api@0b2a2d9 Co-authored-by: Cursor --- src/tracker.test.ts | 139 ++++++++++++++++++++++++++++++++++++++++++++ src/tracker.ts | 22 +++++++ 2 files changed, 161 insertions(+) diff --git a/src/tracker.test.ts b/src/tracker.test.ts index c9ec6e4..ad9907f 100644 --- a/src/tracker.test.ts +++ b/src/tracker.test.ts @@ -53,6 +53,8 @@ describe('TESTING Tracker', () => { describe('WHEN user.identify is called', () => { beforeEach(async () => { + jest.useFakeTimers(); + await tracker.user.identify({ id: 'test-user-id', email: 'test@example.com', @@ -70,6 +72,10 @@ describe('TESTING Tracker', () => { }); }); + afterEach(() => { + jest.useRealTimers(); + }); + it('SHOULD send the request to the default trackingApiBaseUrl', async () => { expect(fetch).toHavePostedTimes( 1, @@ -89,6 +95,52 @@ describe('TESTING Tracker', () => { first_name: 'test', last_name: 'test', }, + extras: { + created_at: new Date().toISOString(), + }, + }, + }, + ); + }); + }); + + describe('WHEN user.identify is called with extras', () => { + beforeEach(async () => { + jest.useFakeTimers(); + + await tracker.user.identify({ + id: 'test-user-id', + email: 'test@example.com', + data: { + first_name: 'test', + }, + extras: { + createdAt: '2024-01-01T00:00:00.000Z', + updateOnly: true, + }, + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('SHOULD send extras as snake_case fields', async () => { + expect(fetch).toHavePostedTimes( + 1, + 'https://api.getvero.com/api/v2/users/track?tracking_api_key=test-api-key', + { + body: { + id: 'test-user-id', + email: 'test@example.com', + channels: [], + data: { + first_name: 'test', + }, + extras: { + created_at: '2024-01-01T00:00:00.000Z', + update_only: true, + }, }, }, ); @@ -263,6 +315,8 @@ describe('TESTING Tracker', () => { describe('WHEN user.identify is called', () => { beforeEach(async () => { + jest.useFakeTimers(); + await tracker.user.identify({ id: 'test-user-id', email: 'test@example.com', @@ -280,6 +334,10 @@ describe('TESTING Tracker', () => { }); }); + afterEach(() => { + jest.useRealTimers(); + }); + it('SHOULD send the request to the specified trackingApiBaseUrl', () => { expect(fetch).toHavePostedTimes( 1, @@ -299,6 +357,9 @@ describe('TESTING Tracker', () => { first_name: 'test', last_name: 'test', }, + extras: { + created_at: new Date().toISOString(), + }, }, }, ); @@ -765,6 +826,9 @@ describe('TESTING Tracker', () => { userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', }, + extras: { + created_at: new Date().toISOString(), + }, }, }, ); @@ -815,6 +879,81 @@ describe('TESTING Tracker', () => { }); }); + describe('WHEN user.identify is called with extras', () => { + beforeEach(async () => { + jest.useFakeTimers(); + + await tracker.user.identify({ + id: 'test-user-id', + email: 'test@example.com', + data: { + first_name: 'test', + }, + extras: { + createdAt: '2024-01-01T00:00:00.000Z', + updateOnly: true, + }, + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('SHOULD send extras on the identify request only', async () => { + expect(fetch).toHavePostedTimes( + 1, + 'https://api.getvero.com/api/v2/users/track?tracking_api_key=test-api-key', + { + body: { + id: 'test-user-id', + email: 'test@example.com', + channels: [], + data: { + first_name: 'test', + language: 'en', + timezone: 10, + ianaTimezone: 'Australia/Sydney', + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', + }, + extras: { + created_at: '2024-01-01T00:00:00.000Z', + update_only: true, + }, + }, + }, + ); + }); + + it('SHOULD still track a "visited site" event with event extras', async () => { + expect(fetch).toHavePostedTimes( + 1, + 'https://api.getvero.com/api/v2/events/track?tracking_api_key=test-api-key', + { + body: { + identity: { + id: 'test-user-id', + email: 'test@example.com', + }, + event_name: 'Visited site', + data: { + language: 'en', + timezone: 10, + ianaTimezone: 'Australia/Sydney', + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', + }, + extras: { + source: 'hostname.example.com', + created_at: new Date().toISOString(), + }, + }, + }, + ); + }); + }); + describe('WHEN user.unidentify is called', () => { beforeEach(async () => { await tracker.user.unidentify(); diff --git a/src/tracker.ts b/src/tracker.ts index a41c44e..cba8f46 100644 --- a/src/tracker.ts +++ b/src/tracker.ts @@ -139,6 +139,22 @@ export interface UserIdentifyRequest extends NoSiteVisitEventRequest { * @example { first_name: "Damien", last_name: "Brzoska", age: 30 } */ data?: Record; + /** + * An object containing key value pairs that represent the reserved, + * Vero-specific `created_at` and `update_only` properties. + * + * @see {@link Tracker#user.identify} + */ + extras?: { + /** + * @example "2023-05-30T04:46:31+0000" + */ + createdAt?: string; + /** + * @example "true" + */ + updateOnly?: boolean; + }; } export interface OptionalUserIdRequest { @@ -318,6 +334,12 @@ class Tracker { email: request.email, channels: request.channels ?? [], data: { ...request.data, ...getDefaultReservedUserData() }, + extras: { + created_at: request.extras?.createdAt ?? new Date().toISOString(), + ...(request.extras?.updateOnly && { + update_only: request.extras.updateOnly, + }), + }, }); this.identityStore?.save(request.id, request.email); From 860c2e6f5c9566fd36d86a4e5a6aabe2f36eb93f Mon Sep 17 00:00:00 2001 From: Yihao Gao Date: Tue, 14 Jul 2026 14:48:41 +1000 Subject: [PATCH 2/2] fix: minor change for updateOnly example value; fixed false updateOnly value will not be included in the request body --- src/tracker.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/tracker.ts b/src/tracker.ts index cba8f46..766ce5a 100644 --- a/src/tracker.ts +++ b/src/tracker.ts @@ -151,7 +151,7 @@ export interface UserIdentifyRequest extends NoSiteVisitEventRequest { */ createdAt?: string; /** - * @example "true" + * @example true */ updateOnly?: boolean; }; @@ -336,9 +336,7 @@ class Tracker { data: { ...request.data, ...getDefaultReservedUserData() }, extras: { created_at: request.extras?.createdAt ?? new Date().toISOString(), - ...(request.extras?.updateOnly && { - update_only: request.extras.updateOnly, - }), + update_only: request.extras?.updateOnly, }, }); this.identityStore?.save(request.id, request.email);