From 333191ef8e0f39c63c0933567c78e723219a94c4 Mon Sep 17 00:00:00 2001 From: atharvnagar-git Date: Thu, 18 Jun 2026 14:24:43 +0530 Subject: [PATCH 1/2] feat(): addition of new folder for the domain name wingify --- .../wingify/__tests__/index.test.ts | 37 ++ .../destinations/wingify/generated-types.ts | 16 + .../identifyUser/__tests__/index.test.ts | 273 ++++++++++ .../wingify/identifyUser/generated-types.ts | 32 ++ .../wingify/identifyUser/index.ts | 100 ++++ .../src/destinations/wingify/index.ts | 88 ++++ .../wingify/pageVisit/__tests__/index.test.ts | 250 ++++++++++ .../wingify/pageVisit/generated-types.ts | 30 ++ .../destinations/wingify/pageVisit/index.ts | 88 ++++ .../syncAudience/__tests__/index.test.ts | 119 +++++ .../wingify/syncAudience/generated-types.ts | 20 + .../wingify/syncAudience/index.ts | 82 +++ .../trackEvent/__tests__/index.test.ts | 469 ++++++++++++++++++ .../wingify/trackEvent/generated-types.ts | 36 ++ .../destinations/wingify/trackEvent/index.ts | 97 ++++ .../src/destinations/wingify/types.ts | 50 ++ .../src/destinations/wingify/utility.ts | 121 +++++ 17 files changed, 1908 insertions(+) create mode 100644 packages/destination-actions/src/destinations/wingify/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/wingify/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/wingify/identifyUser/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/wingify/identifyUser/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/wingify/identifyUser/index.ts create mode 100644 packages/destination-actions/src/destinations/wingify/index.ts create mode 100644 packages/destination-actions/src/destinations/wingify/pageVisit/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/wingify/pageVisit/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/wingify/pageVisit/index.ts create mode 100644 packages/destination-actions/src/destinations/wingify/syncAudience/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/wingify/syncAudience/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/wingify/syncAudience/index.ts create mode 100644 packages/destination-actions/src/destinations/wingify/trackEvent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/wingify/trackEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/wingify/trackEvent/index.ts create mode 100644 packages/destination-actions/src/destinations/wingify/types.ts create mode 100644 packages/destination-actions/src/destinations/wingify/utility.ts diff --git a/packages/destination-actions/src/destinations/wingify/__tests__/index.test.ts b/packages/destination-actions/src/destinations/wingify/__tests__/index.test.ts new file mode 100644 index 00000000000..ed126476da1 --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/__tests__/index.test.ts @@ -0,0 +1,37 @@ +import { createTestIntegration } from '@segment/actions-core' +import Destination from '../index' +import type { Settings } from '../generated-types' +import { generateUUIDFor } from '../utility' + +const testDestination = createTestIntegration(Destination) + +describe('Wingify AccountID Validation', () => { + describe('testAuthentication', () => { + it('should validate authentication inputs', async () => { + const settings: Settings = { + wingifyAccountId: 654331, + region: 'US' + } + await expect(testDestination.testAuthentication(settings)).resolves.not.toThrowError() + }) + + it('should throw error for invalid AccountId', async () => { + const settings: Settings = { + wingifyAccountId: 65431231, + region: 'US' + } + await expect(testDestination.testAuthentication(settings)).rejects.toThrowError() + }) + }) +}) + +describe('UUID Generator', () => { + describe('method: generateFor', () => { + it('should return desired UUID for userId and accountId', () => { + expect(generateUUIDFor('Varun', 12345)).toBe('C4D95C097902569F9A2D2E87CD3201C8') + expect(generateUUIDFor('Alice', 12345)).toBe('E3B732864F315FB6974BC3EF4E2FD920') + expect(generateUUIDFor('__123__', 12345)).toBe('50A5B167FB6356A796F91D8951E480EE') + expect(generateUUIDFor('We@#dcs3232.f3', 12345)).toBe('AAB4580A6BB3525FAA31DC341752D501') + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/wingify/generated-types.ts b/packages/destination-actions/src/destinations/wingify/generated-types.ts new file mode 100644 index 00000000000..11577a19fb4 --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/generated-types.ts @@ -0,0 +1,16 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * Enter your Wingify Account ID + */ + wingifyAccountId: number + /** + * Wingify Fullstack SDK Key. It is mandatory when using the Wingify Fullstack suite. + */ + apikey?: string + /** + * Wingify Region to sync data to. Default is US + */ + region?: string +} diff --git a/packages/destination-actions/src/destinations/wingify/identifyUser/__tests__/index.test.ts b/packages/destination-actions/src/destinations/wingify/identifyUser/__tests__/index.test.ts new file mode 100644 index 00000000000..5750205480e --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/identifyUser/__tests__/index.test.ts @@ -0,0 +1,273 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Destination from '../../index' + +const testDestination = createTestIntegration(Destination) + +const BASE_ENDPOINT = 'https://dev.visualwebsiteoptimizer.com' +const accountId = 654331 +const wingifyUuid = 'ABC123' +const SDK_KEY = 'sample-api-key' +const SANITISED_USERID = '57CC1A3D57215E67824E461010E43F53' + +describe('Wingify.identifyUser Web', () => { + describe('Only required parameters', () => { + it('should send segment traits as Wingify attributes', async () => { + const event = createTestEvent({ + traits: { + wingifyUuid: wingifyUuid, + textProperty: 'Hello' + } + }) + nock(BASE_ENDPOINT).post(`/events/t?en=wingify_syncVisitorProp&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('identifyUser', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: wingifyUuid, + event: { + props: { + $visitor: { + props: { + 'segment.textProperty': 'Hello' + } + }, + wingifyMeta: { + source: 'segment.cloud' + }, + page, + isCustomEvent: true + }, + name: 'wingify_syncVisitorProp' + }, + visitor: { + props: { + 'segment.textProperty': 'Hello' + } + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + expect(responses[0].options.headers).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "user-agent": Array [ + "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + ], + "x-forwarded-for": Array [ + "8.8.8.8", + ], + }, + } + `) + }) + }) + + describe('All parameters', () => { + it('should send segment traits as Wingify attributes', async () => { + const event = createTestEvent({ + traits: { + wingifyUuid: wingifyUuid, + textProperty: 'Hello' + }, + context: { + page: { + url: 'www.abc.com' + }, + ip: '0.0.0.0', + userAgent: 'Segment' + }, + timestamp: '2023-05-09T13:12:44.924Z' + }) + nock(BASE_ENDPOINT).post(`/events/t?en=wingify_syncVisitorProp&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('identifyUser', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: wingifyUuid, + event: { + props: { + $visitor: { + props: { + 'segment.textProperty': 'Hello' + } + }, + wingifyMeta: { + source: 'segment.cloud' + }, + page, + isCustomEvent: true + }, + name: 'wingify_syncVisitorProp' + }, + visitor: { + props: { + 'segment.textProperty': 'Hello' + } + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + expect(responses[0].options.headers).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "user-agent": Array [ + "Segment", + ], + "x-forwarded-for": Array [ + "0.0.0.0", + ], + }, + } + `) + }) + }) +}) + +describe('Wingify.identifyUser Fullstack', () => { + describe('Only required parameters', () => { + it('should send segment traits as Wingify attributes', async () => { + const event = createTestEvent({ + traits: { + wingifyUuid: wingifyUuid, + textProperty: 'Hello' + } + }) + nock(BASE_ENDPOINT).post(`/events/t?en=wingify_syncVisitorProp&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('identifyUser', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId, + apikey: SDK_KEY + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: SANITISED_USERID, + event: { + props: { + $visitor: { + props: { + 'segment.textProperty': 'Hello', + wingify_fs_environment: 'sample-api-key' + } + }, + wingifyMeta: { + source: 'segment.cloud' + }, + page, + isCustomEvent: true + }, + name: 'wingify_syncVisitorProp' + }, + visitor: { + props: { + 'segment.textProperty': 'Hello', + wingify_fs_environment: 'sample-api-key' + } + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + expect(responses[0].options.headers).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "user-agent": Array [ + "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + ], + "x-forwarded-for": Array [ + "8.8.8.8", + ], + }, + } + `) + }) + }) + + describe('All parameters', () => { + it('should send segment traits as Wingify attributes', async () => { + const event = createTestEvent({ + traits: { + wingifyUuid: wingifyUuid, + textProperty: 'Hello' + }, + context: { + page: { + url: 'www.abc.com' + }, + ip: '0.0.0.0', + userAgent: 'Segment' + }, + timestamp: '2023-05-09T13:12:44.924Z' + }) + nock(BASE_ENDPOINT).post(`/events/t?en=wingify_syncVisitorProp&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('identifyUser', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId, + apikey: SDK_KEY + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: SANITISED_USERID, + event: { + props: { + $visitor: { + props: { + 'segment.textProperty': 'Hello', + wingify_fs_environment: 'sample-api-key' + } + }, + wingifyMeta: { + source: 'segment.cloud' + }, + page, + isCustomEvent: true + }, + name: 'wingify_syncVisitorProp' + }, + visitor: { + props: { + 'segment.textProperty': 'Hello', + wingify_fs_environment: 'sample-api-key' + } + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + expect(responses[0].options.headers).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "user-agent": Array [ + "Segment", + ], + "x-forwarded-for": Array [ + "0.0.0.0", + ], + }, + } + `) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/wingify/identifyUser/generated-types.ts b/packages/destination-actions/src/destinations/wingify/identifyUser/generated-types.ts new file mode 100644 index 00000000000..ba88835eaf2 --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/identifyUser/generated-types.ts @@ -0,0 +1,32 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Visitor's attributes to be mapped + */ + attributes: { + [k: string]: unknown + } + /** + * Wingify UUID + */ + wingifyUuid: string + /** + * Contains context information regarding a webpage + */ + page?: { + [k: string]: unknown + } + /** + * IP address of the user + */ + ip?: string + /** + * User-Agent of the user + */ + userAgent?: string + /** + * Timestamp on the event + */ + timestamp?: string +} diff --git a/packages/destination-actions/src/destinations/wingify/identifyUser/index.ts b/packages/destination-actions/src/destinations/wingify/identifyUser/index.ts new file mode 100644 index 00000000000..b80f6c99f3b --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/identifyUser/index.ts @@ -0,0 +1,100 @@ +import { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import {formatPayload, formatAttributes, hosts} from '../utility' + +const action: ActionDefinition = { + title: 'Identify User', + description: "Maps Segment's visitor traits to the visitor attributes in Wingify", + defaultSubscription: 'type = "identify"', + fields: { + attributes: { + description: `Visitor's attributes to be mapped`, + label: 'attributes', + required: true, + type: 'object', + default: { + '@path': '$.traits' + } + }, + wingifyUuid: { + description: 'Wingify UUID', + label: 'Wingify UUID', + required: true, + type: 'string', + default: { + '@path': '$.traits.wingify_uuid' + } + }, + page: { + description: 'Contains context information regarding a webpage', + label: 'Page', + required: false, + type: 'object', + default: { + '@path': '$.context.page' + } + }, + ip: { + description: 'IP address of the user', + label: 'IP Address', + required: false, + type: 'string', + default: { + '@path': '$.context.ip' + } + }, + userAgent: { + description: 'User-Agent of the user', + label: 'User Agent', + required: false, + type: 'string', + default: { + '@path': '$.context.userAgent' + } + }, + timestamp: { + description: 'Timestamp on the event', + label: 'Timestamp', + required: false, + type: 'string', + default: { + '@path': '$.timestamp' + } + } + }, + perform: (request, { settings, payload }) => { + const eventName = 'wingify_syncVisitorProp' + const attributes = payload.attributes + delete attributes['wingify_uuid'] + const formattedAttributes = formatAttributes(attributes) + const visitor = { props: formattedAttributes } + const { headers, structuredPayload } = formatPayload( + eventName, + payload, + true, + false, + settings.apikey, + settings.wingifyAccountId + ) + if (structuredPayload.d.visitor && structuredPayload.d.event.props.$visitor) { + structuredPayload.d.visitor.props = { + ...structuredPayload.d.visitor.props, + ...formattedAttributes + } + } else { + structuredPayload.d.visitor = visitor + structuredPayload.d.event.props.$visitor = visitor + } + const region = settings.region || "US" + const host = hosts[region] + const endpoint = `${host}/events/t?en=${eventName}&a=${settings.wingifyAccountId}` + return request(endpoint, { + method: 'POST', + json: structuredPayload, + headers + }) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/wingify/index.ts b/packages/destination-actions/src/destinations/wingify/index.ts new file mode 100644 index 00000000000..879125e0789 --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/index.ts @@ -0,0 +1,88 @@ +import type { DestinationDefinition } from '@segment/actions-core' +import type { Settings } from './generated-types' +import { defaultValues } from '@segment/actions-core' + +import pageVisit from './pageVisit' +import trackEvent from './trackEvent' +import identifyUser from './identifyUser' +import syncAudience from './syncAudience' + +const destination: DestinationDefinition = { + name: 'Wingify Cloud Mode (Actions)', + slug: 'actions-wingify-cloud', + mode: 'cloud', + presets: [ + { + name: 'Track Event', + subscribe: 'type = "track"', + partnerAction: 'trackEvent', + mapping: defaultValues(trackEvent.fields), + type: 'automatic' + }, + { + name: 'Identify User', + subscribe: 'type = "identify"', + partnerAction: 'identifyUser', + mapping: defaultValues(identifyUser.fields), + type: 'automatic' + }, + { + name: 'Page Visit', + subscribe: 'type = "page"', + partnerAction: 'pageVisit', + mapping: defaultValues(pageVisit.fields), + type: 'automatic' + }, + { + name: 'Sync Audience', + subscribe: 'event = "Audience Entered" or event = "Audience Exited"', + partnerAction: 'syncAudience', + mapping: defaultValues(syncAudience.fields), + type: 'automatic' + } + ], + authentication: { + scheme: 'custom', + fields: { + vwoAccountId: { + label: 'Your Wingify account ID', + description: 'Enter your Wingify Account ID', + type: 'number', + required: true + }, + apikey: { + label: 'Wingify SDK Key', + description: 'Wingify Fullstack SDK Key. It is mandatory when using the Wingify Fullstack suite.', + type: 'password', + required: false + }, + region: { + label: 'Region', + description: 'Wingify Region to sync data to. Default is US', + type: 'string', + choices: [ + { label: 'US', value: 'US' }, + { label: 'Europe', value: 'EU' }, + { label: 'Asia', value: 'AS' } + ], + default: 'US' + } + }, + testAuthentication: (_request, { settings }) => { + if (settings.wingifyAccountId < 1 || settings.wingifyAccountId.toString().length > 7) { + throw new Error('Invalid AccountID. Please check your AccountID') + } else { + return true + } + } + }, + + actions: { + trackEvent, + identifyUser, + pageVisit, + syncAudience + } +} + +export default destination diff --git a/packages/destination-actions/src/destinations/wingify/pageVisit/__tests__/index.test.ts b/packages/destination-actions/src/destinations/wingify/pageVisit/__tests__/index.test.ts new file mode 100644 index 00000000000..c72f63825bb --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/pageVisit/__tests__/index.test.ts @@ -0,0 +1,250 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Destination from '../../index' + +const testDestination = createTestIntegration(Destination) + +const BASE_ENDPOINT = 'https://dev.visualwebsiteoptimizer.com' +const accountId = 654331 +const wingifyUuid = 'ABC123' +const EVENT_NAME = 'segment.pageView' +const SDK_KEY = 'sample-api-key' +const SANITISED_USERID = '57CC1A3D57215E67824E461010E43F53' + +describe('Wingify.pageVisit Web', () => { + describe('Only required parameters', () => { + it('should send Page Visit event to Wingify', async () => { + const event = createTestEvent({ + properties: { + wingifyUuid: wingifyUuid + } + }) + nock(BASE_ENDPOINT).post(`/events/t?en=${EVENT_NAME}&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('pageVisit', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: wingifyUuid, + event: { + props: { + url: page?.url, + page, + isCustomEvent: false, + wingifyMeta: { + metric: {} + } + }, + name: EVENT_NAME + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + expect(responses[0].options.headers).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "user-agent": Array [ + "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + ], + "x-forwarded-for": Array [ + "8.8.8.8", + ], + }, + } + `) + }) + }) + + describe('All Parameters', () => { + it('should send Page Visit event to Wingify', async () => { + const event = createTestEvent({ + properties: { + wingifyUuid: wingifyUuid + }, + context: { + page: { + url: 'www.abc.com' + }, + ip: '0.0.0.0', + userAgent: 'Segment' + }, + timestamp: '2023-05-09T13:12:44.924Z' + }) + nock(BASE_ENDPOINT).post(`/events/t?en=${EVENT_NAME}&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('pageVisit', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: wingifyUuid, + event: { + props: { + url: 'www.abc.com', + page, + isCustomEvent: false, + wingifyMeta: { + metric: {} + } + }, + name: EVENT_NAME + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + expect(responses[0].options.headers).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "user-agent": Array [ + "Segment", + ], + "x-forwarded-for": Array [ + "0.0.0.0", + ], + }, + } + `) + }) + }) +}) + +describe('Wingify.pageVisit Fullstack', () => { + describe('Only required parameters', () => { + it('should send Page Visit event to Wingify', async () => { + const event = createTestEvent({ + properties: { + wingifyUuid: wingifyUuid + } + }) + nock(BASE_ENDPOINT).post(`/events/t?en=${EVENT_NAME}&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('pageVisit', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId, + apikey: SDK_KEY + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: SANITISED_USERID, + event: { + props: { + url: page?.url, + page, + isCustomEvent: false, + $visitor: { + props: { + wingify_fs_environment: 'sample-api-key' + } + }, + wingifyMeta: { + metric: {} + } + }, + name: EVENT_NAME + }, + visitor: { + props: { + wingify_fs_environment: 'sample-api-key' + } + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + expect(responses[0].options.headers).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "user-agent": Array [ + "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + ], + "x-forwarded-for": Array [ + "8.8.8.8", + ], + }, + } + `) + }) + }) + + describe('All parameters', () => { + it('should send Page Visit event to Wingify', async () => { + const event = createTestEvent({ + properties: { + wingifyUuid: wingifyUuid + }, + context: { + page: { + url: 'www.abc.com' + }, + ip: '0.0.0.0', + userAgent: 'Segment' + }, + timestamp: '2023-05-09T13:12:44.924Z' + }) + nock(BASE_ENDPOINT).post(`/events/t?en=${EVENT_NAME}&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('pageVisit', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId, + apikey: SDK_KEY + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: SANITISED_USERID, + event: { + props: { + url: 'www.abc.com', + page, + isCustomEvent: false, + $visitor: { + props: { + wingify_fs_environment: 'sample-api-key' + } + }, + wingifyMeta: { + metric: {} + } + }, + name: EVENT_NAME + }, + visitor: { + props: { + wingify_fs_environment: 'sample-api-key' + } + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + expect(responses[0].options.headers).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "user-agent": Array [ + "Segment", + ], + "x-forwarded-for": Array [ + "0.0.0.0", + ], + }, + } + `) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/wingify/pageVisit/generated-types.ts b/packages/destination-actions/src/destinations/wingify/pageVisit/generated-types.ts new file mode 100644 index 00000000000..9fab9c51279 --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/pageVisit/generated-types.ts @@ -0,0 +1,30 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * URL of the webpage + */ + url?: string + /** + * Wingify UUID + */ + wingifyUuid: string + /** + * Contains context information regarding a webpage + */ + page?: { + [k: string]: unknown + } + /** + * IP address of the user + */ + ip?: string + /** + * User-Agent of the user + */ + userAgent?: string + /** + * Timestamp on the event + */ + timestamp?: string +} diff --git a/packages/destination-actions/src/destinations/wingify/pageVisit/index.ts b/packages/destination-actions/src/destinations/wingify/pageVisit/index.ts new file mode 100644 index 00000000000..f87fe56967f --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/pageVisit/index.ts @@ -0,0 +1,88 @@ +import { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { formatPayload, hosts, sanitiseEventName } from '../utility' + +const action: ActionDefinition = { + title: 'Page Visit', + description: `Sends Segment's page event to Wingify`, + fields: { + url: { + description: 'URL of the webpage', + label: 'Page URL', + required: false, + type: 'string', + default: { + '@path': '$.context.page.url' + } + }, + wingifyUuid: { + description: 'Wingify UUID', + label: 'Wingify UUID', + required: true, + type: 'string', + default: { + '@path': '$.properties.wingify_uuid' + } + }, + page: { + description: 'Contains context information regarding a webpage', + label: 'Page', + required: false, + type: 'object', + default: { + '@path': '$.context.page' + } + }, + ip: { + description: 'IP address of the user', + label: 'IP Address', + required: false, + type: 'string', + default: { + '@path': '$.context.ip' + } + }, + userAgent: { + description: 'User-Agent of the user', + label: 'User Agent', + required: false, + type: 'string', + default: { + '@path': '$.context.userAgent' + } + }, + timestamp: { + description: 'Timestamp on the event', + label: 'Timestamp', + required: false, + type: 'string', + default: { + '@path': '$.timestamp' + } + } + }, + defaultSubscription: 'type = "page"', + perform: (request, { settings, payload }) => { + const eventName = sanitiseEventName('pageView') + const { headers, structuredPayload } = formatPayload( + eventName, + payload, + false, + false, + settings.apikey, + settings.wingifyAccountId + ) + structuredPayload.d.event.props['url'] = payload.url + const region = settings.region || 'US' + const host = hosts[region] + const endpoint = `${host}/events/t?en=${eventName}&a=${settings.wingifyAccountId}` + return request(endpoint, { + method: 'POST', + json: structuredPayload, + headers + }) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/wingify/syncAudience/__tests__/index.test.ts b/packages/destination-actions/src/destinations/wingify/syncAudience/__tests__/index.test.ts new file mode 100644 index 00000000000..4addab4e628 --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/syncAudience/__tests__/index.test.ts @@ -0,0 +1,119 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Destination from '../../index' + +const testDestination = createTestIntegration(Destination) + +const BASE_ENDPOINT = 'https://dev.visualwebsiteoptimizer.com' +const accountId = 654331 + +describe('Wingify.syncAudience', () => { + it('should send the add audience call', async () => { + const event = createTestEvent({ + event: 'Audience Entered', + userId: 'test_user', + properties: { + audience_key: 'test_audience' + } + }) + nock(BASE_ENDPOINT).post(`/events/t?en=wingify_integration&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('syncAudience', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId, + apikey: '' + } + }) + const expectedRequest = { + d: { + event: { + name: 'wingify_integration', + props: { + action: 'audience_entered', + audienceName: 'test_audience', + audienceId: 'test_audience', + identifier: 'test_user', + accountId: 654331, + integration: 'segment' + } + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + }) + + it('should handle anonymous users', async () => { + const event = createTestEvent({ + event: 'Audience Entered', + userId: null, + anonymousId: 'anonymous-id', + properties: { + audience_key: 'test_audience' + } + }) + nock(BASE_ENDPOINT).post(`/events/t?en=wingify_integration&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('syncAudience', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId, + apikey: '' + } + }) + const expectedRequest = { + d: { + event: { + name: 'wingify_integration', + props: { + action: 'audience_entered', + audienceName: 'test_audience', + audienceId: 'test_audience', + identifier: 'anonymous-id', + accountId: 654331, + integration: 'segment' + } + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + }) + + it('should send the remove audience call', async () => { + const event = createTestEvent({ + event: 'Audience Exited', + userId: 'test_user', + properties: { + audience_key: 'test_audience' + } + }) + nock(BASE_ENDPOINT).post(`/events/t?en=wingify_integration&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('syncAudience', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId, + apikey: '' + } + }) + const expectedRequest = { + d: { + event: { + name: 'wingify_integration', + props: { + action: 'audience_exited', + audienceName: 'test_audience', + audienceId: 'test_audience', + identifier: 'test_user', + accountId: 654331, + integration: 'segment' + } + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + }) +}) diff --git a/packages/destination-actions/src/destinations/wingify/syncAudience/generated-types.ts b/packages/destination-actions/src/destinations/wingify/syncAudience/generated-types.ts new file mode 100644 index 00000000000..83254d24b07 --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/syncAudience/generated-types.ts @@ -0,0 +1,20 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Name of the event + */ + name: string + /** + * An unique identifier for the user + */ + userId?: string + /** + * Anonymous ID for users + */ + anonymousId?: string + /** + * Segment's audience ID + */ + audienceId: string +} diff --git a/packages/destination-actions/src/destinations/wingify/syncAudience/index.ts b/packages/destination-actions/src/destinations/wingify/syncAudience/index.ts new file mode 100644 index 00000000000..71382144216 --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/syncAudience/index.ts @@ -0,0 +1,82 @@ +import type { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import {hosts} from "../utility"; + +const action: ActionDefinition = { + title: 'Sync Audience', + description: 'Syncs Segment audiences to Wingify', + defaultSubscription: 'event = "Audience Entered" or event = "Audience Exited"', + fields: { + name: { + description: 'Name of the event', + label: 'Event Name', + required: true, + type: 'string', + default: { + '@path': '$.event' + } + }, + userId: { + description: 'An unique identifier for the user', + label: 'User ID', + type: 'string', + default: { + '@path': '$.userId' + } + }, + anonymousId: { + description: 'Anonymous ID for users', + label: 'Anonymous ID', + type: 'string', + default: { + '@path': '$.anonymousId' + } + }, + audienceId: { + description: "Segment's audience ID", + label: 'Audience ID', + required: true, + type: 'string', + default: { + '@path': '$.properties.audience_key' + } + } + }, + perform: (request, { settings, payload }) => { + const epochTime = new Date().valueOf() + const time = Math.floor(epochTime) + let action + if (payload.name == 'Audience Entered') { + action = 'audience_entered' + } else if (payload.name == 'Audience Exited') { + action = 'audience_exited' + } + const wingifyPayload = { + d: { + event: { + name: 'wingify_integration', + time, + props: { + action, + audienceName: payload.audienceId, + audienceId: payload.audienceId, + identifier: payload.userId || payload.anonymousId, + accountId: settings.wingifyAccountId, + integration: 'segment' + } + } + } + } + const region = settings.region || "US" + const host = hosts[region] + const endpoint = `${host}/events/t?en=wingify_integration&a=${settings.wingifyAccountId}` + + return request(endpoint, { + method: 'POST', + json: wingifyPayload + }) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/wingify/trackEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/wingify/trackEvent/__tests__/index.test.ts new file mode 100644 index 00000000000..3897025ebee --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/trackEvent/__tests__/index.test.ts @@ -0,0 +1,469 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Destination from '../../index' + +const testDestination = createTestIntegration(Destination) + +const BASE_ENDPOINT = 'https://dev.visualwebsiteoptimizer.com' +const accountId = 654331 +const wingifyUuid = 'ABC123' +const SDK_KEY = 'sample-api-key' +const SANITISED_USERID = '57CC1A3D57215E67824E461010E43F53' + +describe('Wingify.trackEvent Web', () => { + describe('Only required parameters', () => { + it('should send send event call to Wingify', async () => { + const event = createTestEvent({ + event: 'testEvent', + properties: { + wingifyUuid: wingifyUuid + } + }) + nock(BASE_ENDPOINT).post(`/events/t?en=segment.testEvent&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId, + apikey: '' + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: wingifyUuid, + event: { + props: { + page, + isCustomEvent: true, + wingifyMeta: { + source: 'segment.cloud', + ogName: 'testEvent', + metric: {} + } + }, + name: 'segment.testEvent' + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + expect(responses[0].options.headers).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "user-agent": Array [ + "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + ], + "x-forwarded-for": Array [ + "8.8.8.8", + ], + }, + } + `) + }) + + it('should send segment properties as Wingify properties', async () => { + const event = createTestEvent({ + event: 'testEvent', + properties: { + wingifyUuid: wingifyUuid, + amount: 100, + currency: 'INR', + outbound: true + } + }) + nock(BASE_ENDPOINT).post(`/events/t?en=segment.testEvent&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: wingifyUuid, + event: { + props: { + amount: 100, + currency: 'INR', + outbound: true, + page, + isCustomEvent: true, + wingifyMeta: { + source: 'segment.cloud', + ogName: 'testEvent', + metric: {} + } + }, + name: 'segment.testEvent' + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + }) + }) + + describe('All parameters', () => { + it('should send send event call to Wingify', async () => { + const event = createTestEvent({ + event: 'testEvent', + properties: { + wingifyUuid: wingifyUuid + }, + context: { + page: { + url: 'www.abc.com' + }, + ip: '0.0.0.0', + userAgent: 'Segment' + }, + timestamp: '2023-05-09T13:12:44.924Z' + }) + nock(BASE_ENDPOINT).post(`/events/t?en=segment.testEvent&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId, + apikey: '' + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: wingifyUuid, + event: { + props: { + page, + isCustomEvent: true, + wingifyMeta: { + source: 'segment.cloud', + ogName: 'testEvent', + metric: {} + } + }, + name: 'segment.testEvent' + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + expect(responses[0].options.headers).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "user-agent": Array [ + "Segment", + ], + "x-forwarded-for": Array [ + "0.0.0.0", + ], + }, + } + `) + }) + + it('should send segment properties as Wingify properties', async () => { + const event = createTestEvent({ + event: 'testEvent', + properties: { + wingifyUuid: wingifyUuid, + amount: 100, + currency: 'INR', + outbound: true + }, + context: { + page: { + url: 'www.abc.com' + }, + ip: '0.0.0.0', + userAgent: 'Segment' + }, + timestamp: '2023-05-09T13:12:44.924Z' + }) + nock(BASE_ENDPOINT).post(`/events/t?en=segment.testEvent&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: wingifyUuid, + event: { + props: { + amount: 100, + currency: 'INR', + outbound: true, + page, + isCustomEvent: true, + wingifyMeta: { + source: 'segment.cloud', + ogName: 'testEvent', + metric: {} + } + }, + name: 'segment.testEvent' + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + }) + }) +}) + +describe('Wingify.trackEvent Fullstack', () => { + describe('Only required parameters', () => { + it('should send send event call to Wingify', async () => { + const event = createTestEvent({ + event: 'testEvent', + properties: { + wingifyUuid: wingifyUuid + } + }) + nock(BASE_ENDPOINT).post(`/events/t?en=segment.testEvent&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId, + apikey: SDK_KEY + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: SANITISED_USERID, + event: { + props: { + page, + isCustomEvent: true, + $visitor: { + props: { + wingify_fs_environment: 'sample-api-key' + } + }, + wingifyMeta: { + source: 'segment.cloud', + ogName: 'testEvent', + metric: {} + } + }, + name: 'segment.testEvent' + }, + visitor: { + props: { + wingify_fs_environment: 'sample-api-key' + } + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + expect(responses[0].options.headers).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "user-agent": Array [ + "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + ], + "x-forwarded-for": Array [ + "8.8.8.8", + ], + }, + } + `) + }) + + it('should send segment properties as Wingify properties', async () => { + const event = createTestEvent({ + event: 'testEvent', + properties: { + wingifyUuid: wingifyUuid, + amount: 100, + currency: 'INR', + outbound: true + } + }) + nock(BASE_ENDPOINT).post(`/events/t?en=segment.testEvent&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId, + apikey: SDK_KEY + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: SANITISED_USERID, + event: { + props: { + amount: 100, + currency: 'INR', + outbound: true, + page, + isCustomEvent: true, + $visitor: { + props: { + wingify_fs_environment: 'sample-api-key' + } + }, + wingifyMeta: { + source: 'segment.cloud', + ogName: 'testEvent', + metric: {} + } + }, + name: 'segment.testEvent' + }, + visitor: { + props: { + wingify_fs_environment: 'sample-api-key' + } + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + }) + }) + + describe('All Parameters', () => { + it('should send send event call to Wingify', async () => { + const event = createTestEvent({ + event: 'testEvent', + properties: { + wingifyUuid: wingifyUuid + }, + context: { + page: { + url: 'www.abc.com' + }, + ip: '0.0.0.0', + userAgent: 'Segment' + }, + timestamp: '2023-05-09T13:12:44.924Z' + }) + nock(BASE_ENDPOINT).post(`/events/t?en=segment.testEvent&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId, + apikey: SDK_KEY + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: SANITISED_USERID, + event: { + props: { + page, + isCustomEvent: true, + $visitor: { + props: { + wingify_fs_environment: 'sample-api-key' + } + }, + wingifyMeta: { + source: 'segment.cloud', + ogName: 'testEvent', + metric: {} + } + }, + name: 'segment.testEvent' + }, + visitor: { + props: { + wingify_fs_environment: 'sample-api-key' + } + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + expect(responses[0].options.headers).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "user-agent": Array [ + "Segment", + ], + "x-forwarded-for": Array [ + "0.0.0.0", + ], + }, + } + `) + }) + + it('should send segment properties as Wingify properties', async () => { + const event = createTestEvent({ + event: 'testEvent', + properties: { + wingifyUuid: wingifyUuid, + amount: 100, + currency: 'INR', + outbound: true + }, + context: { + page: { + url: 'www.abc.com' + }, + ip: '0.0.0.0', + userAgent: 'Segment' + }, + timestamp: '2023-05-09T13:12:44.924Z' + }) + nock(BASE_ENDPOINT).post(`/events/t?en=segment.testEvent&a=${accountId}`).reply(200, {}) + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true, + settings: { + wingifyAccountId: accountId, + apikey: SDK_KEY + } + }) + const page = event.context?.page + const expectedRequest = { + d: { + visId: SANITISED_USERID, + event: { + props: { + amount: 100, + currency: 'INR', + outbound: true, + page, + isCustomEvent: true, + $visitor: { + props: { + wingify_fs_environment: 'sample-api-key' + } + }, + wingifyMeta: { + source: 'segment.cloud', + ogName: 'testEvent', + metric: {} + } + }, + name: 'segment.testEvent' + }, + visitor: { + props: { + wingify_fs_environment: 'sample-api-key' + } + } + } + } + expect(responses[0].status).toBe(200) + expect(responses[0].options.json).toMatchObject(expectedRequest) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/wingify/trackEvent/generated-types.ts b/packages/destination-actions/src/destinations/wingify/trackEvent/generated-types.ts new file mode 100644 index 00000000000..e5ec3a1b812 --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/trackEvent/generated-types.ts @@ -0,0 +1,36 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Name of the event + */ + name: string + /** + * JSON object containing additional properties that will be associated with the event. + */ + properties?: { + [k: string]: unknown + } + /** + * Wingify UUID + */ + wingifyUuid: string + /** + * Contains context information regarding a webpage + */ + page?: { + [k: string]: unknown + } + /** + * IP address of the user + */ + ip?: string + /** + * User-Agent of the user + */ + userAgent?: string + /** + * Timestamp on the event + */ + timestamp?: string +} diff --git a/packages/destination-actions/src/destinations/wingify/trackEvent/index.ts b/packages/destination-actions/src/destinations/wingify/trackEvent/index.ts new file mode 100644 index 00000000000..2461457e843 --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/trackEvent/index.ts @@ -0,0 +1,97 @@ +import { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { formatPayload, sanitiseEventName, hosts } from '../utility' + +const action: ActionDefinition = { + title: 'Track Event', + description: `Sends Segment's track event to Wingify`, + defaultSubscription: 'type = "track"', + fields: { + name: { + description: 'Name of the event', + label: 'Name', + required: true, + type: 'string', + default: { + '@path': '$.event' + } + }, + properties: { + description: 'JSON object containing additional properties that will be associated with the event.', + label: 'Properties', + required: false, + type: 'object', + default: { + '@path': '$.properties' + } + }, + wingifyUuid: { + description: 'Wingify UUID', + label: 'Wingify UUID', + required: true, + type: 'string', + default: { + '@path': '$.properties.wingify_uuid' + } + }, + page: { + description: 'Contains context information regarding a webpage', + label: 'Page', + required: false, + type: 'object', + default: { + '@path': '$.context.page' + } + }, + ip: { + description: 'IP address of the user', + label: 'IP Address', + required: false, + type: 'string', + default: { + '@path': '$.context.ip' + } + }, + userAgent: { + description: 'User-Agent of the user', + label: 'User Agent', + required: false, + type: 'string', + default: { + '@path': '$.context.userAgent' + } + }, + timestamp: { + description: 'Timestamp on the event', + label: 'Timestamp', + required: false, + type: 'string', + default: { + '@path': '$.timestamp' + } + } + }, + perform: (request, { settings, payload }) => { + const sanitisedEventName = sanitiseEventName(payload.name) + const { headers, structuredPayload } = formatPayload( + sanitisedEventName, + payload, + true, + true, + settings.apikey, + settings.wingifyAccountId + ) + structuredPayload.d.event.props.wingifyMeta['ogName'] = payload.name + const region = settings.region || "US" + const host = hosts[region] + const endpoint = `${host}/events/t?en=${sanitisedEventName}&a=${settings.wingifyAccountId}` + return request(endpoint, { + method: 'POST', + headers, + json: structuredPayload + }) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/wingify/types.ts b/packages/destination-actions/src/destinations/wingify/types.ts new file mode 100644 index 00000000000..9f01db8e54f --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/types.ts @@ -0,0 +1,50 @@ +export type wingifyPayload = { + d: { + msgId: string + visId: string + event: { + props: { + wingify_og_event?: string + $visitor?: { + props: { + [k: string]: unknown + } + } + page: { + [k: string]: unknown + } + [k: string]: unknown + isCustomEvent?: boolean + wingifyMeta: { + source: string + ogName?: string + [k: string]: unknown + } + } + name: string + time: number + } + visitor?: { + props: { + [k: string]: unknown + } + } + sessionId: number + } +} + +export type commonPayload = { + properties?: { + [k: string]: unknown + } + attributes?: { + [k: string]: unknown + } + wingifyUuid: string + page?: { + [k: string]: unknown + } + ip?: string + userAgent?: string + timestamp?: string +} diff --git a/packages/destination-actions/src/destinations/wingify/utility.ts b/packages/destination-actions/src/destinations/wingify/utility.ts new file mode 100644 index 00000000000..82eedddab7c --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/utility.ts @@ -0,0 +1,121 @@ +import type { commonPayload, wingifyPayload } from './types' +import * as crypto from 'crypto' + +const namespace = '11e13cd7-6c48-53ec-8679-7e9c752273c5' + +export const hosts: { [key: string]: string } = { + US: 'https://eadge.wingify.com', + EU: 'https://eadge.wingify.com/eu01', + AS: 'https://eadge.wingify.com/as01' +} + +function uuidv5(name: string, namespace: string): string { + const namespaceBuffer = Buffer.from(namespace.replace(/-/g, ''), 'hex') + const nameBuffer = Buffer.from(name) + const hashBuffer = crypto.createHash('sha1').update(namespaceBuffer).update(nameBuffer).digest() + hashBuffer[6] &= 0x0f + hashBuffer[6] |= 0x50 + hashBuffer[8] &= 0x3f + hashBuffer[8] |= 0x80 + const uuidSegments = [ + hashBuffer.subarray(0, 4).toString('hex'), + hashBuffer.subarray(4, 6).toString('hex'), + hashBuffer.subarray(6, 8).toString('hex'), + hashBuffer.subarray(8, 10).toString('hex'), + hashBuffer.subarray(10, 16).toString('hex') + ] + return uuidSegments.join('-') +} + +function generate(name: string, namespace: string): string { + if (!name || !namespace) { + return '' + } + return uuidv5(name, namespace) +} + +export function formatPayload( + name: string, + payload: commonPayload, + isCustomEvent: boolean, + isTrack = false, + apiKey = '', + accountId = 0 +) { + let formattedProperties: { [k: string]: unknown } = {} + const wingifyUuid = apiKey.trim().length ? generateUUIDFor(payload.wingifyUuid, accountId) : payload.wingifyUuid + if (isTrack) { + formattedProperties = { ...payload.properties } + delete formattedProperties['wingify_uuid'] + } + const epochTime = new Date().valueOf() + const time = Math.floor(epochTime) + const sessionId = Math.floor(epochTime / 1000) + const page = payload.page + ? { + ...payload.page, + referrerUrl: payload.page['referrer'] + } + : {} + const structuredPayload: wingifyPayload = { + d: { + msgId: `${wingifyUuid}-${sessionId}`, + visId: wingifyUuid, + event: { + props: { + ...formattedProperties, + page, + isCustomEvent, + wingifyMeta: { + source: 'segment.cloud', + metric: {} + } + }, + name, + time + }, + sessionId + } + } + const headers: { [k: string]: string } = payload.userAgent + ? { + 'User-Agent': payload.userAgent + } + : {} + + if (apiKey.trim().length) { + const visitorObj = { + props: { + wingify_fs_environment: apiKey + } + } + structuredPayload.d.event.props.$visitor = visitorObj + structuredPayload.d.visitor = visitorObj + } + + if (payload.ip) { + headers['X-Forwarded-For'] = payload.ip + } + return { headers, structuredPayload } +} + +export function sanitiseEventName(name: string) { + return 'segment.' + name +} + +export function formatAttributes(attributes: { [k: string]: unknown } | undefined) { + const formattedAttributes: { [k: string]: unknown } = {} + for (const key in attributes) { + formattedAttributes[`segment.${key}`] = attributes[key] + } + return formattedAttributes +} + +export function generateUUIDFor(userId: string | number, accountId: number) { + userId = `${userId}` // type-cast + const hash = `${accountId}` + const userIdNamespace = generate(hash, namespace) + const uuidForUserIdAccountId = generate(userId, userIdNamespace) + const desiredUuid = uuidForUserIdAccountId.replace(/-/gi, '').toUpperCase() + return desiredUuid +} From fd2ec3de09eb300acfa341bc5c5278859034af9a Mon Sep 17 00:00:00 2001 From: Zeeshan Date: Tue, 23 Jun 2026 12:20:20 +0530 Subject: [PATCH 2/2] fix(wingify): address PR #3839 review and use collect.wingify.net endpoints Align auth fields, audience sync, tests, and registration with reviewer feedback, and point all regions at collect.wingify.net. Co-authored-by: Cursor --- .../src/destinations/index.ts | 2 + .../wingify/__tests__/index.test.ts | 4 +- .../identifyUser/__tests__/index.test.ts | 10 +- .../wingify/identifyUser/generated-types.ts | 2 +- .../wingify/identifyUser/index.ts | 7 +- .../src/destinations/wingify/index.ts | 8 +- .../src/destinations/wingify/metadata.json | 833 ++++++++++++++++++ .../wingify/pageVisit/__tests__/index.test.ts | 10 +- .../wingify/pageVisit/generated-types.ts | 2 +- .../destinations/wingify/pageVisit/index.ts | 3 +- .../syncAudience/__tests__/index.test.ts | 65 +- .../wingify/syncAudience/index.ts | 21 +- .../trackEvent/__tests__/index.test.ts | 18 +- .../wingify/trackEvent/generated-types.ts | 2 +- .../destinations/wingify/trackEvent/index.ts | 5 +- .../src/destinations/wingify/utility.ts | 9 +- 16 files changed, 934 insertions(+), 67 deletions(-) create mode 100644 packages/destination-actions/src/destinations/wingify/metadata.json diff --git a/packages/destination-actions/src/destinations/index.ts b/packages/destination-actions/src/destinations/index.ts index 7df8a443fed..bf1bd1fa91d 100644 --- a/packages/destination-actions/src/destinations/index.ts +++ b/packages/destination-actions/src/destinations/index.ts @@ -77,6 +77,8 @@ register('63936c37dbc54a052e34e30e', './google-sheets-dev') register('63872c01c0c112b9b4d75412', './braze-cohorts') register('639c2dbb1309fdcad13951b6', './segment-profiles') register('63bedc136a8484a53739e013', './vwo') +// TODO: Replace placeholder metadata ID with the production ID from Segment (create in prod, sync via sprout). +register('67daf8c4e2b1a403f9c8d52e', './wingify') register('63d17a1e6ab3e62212278cd0', './saleswings') register('63e42aa0ed203bc54eaabbee', './launchpad') register('63e42b47479274407b671071', './livelike-cloud') diff --git a/packages/destination-actions/src/destinations/wingify/__tests__/index.test.ts b/packages/destination-actions/src/destinations/wingify/__tests__/index.test.ts index ed126476da1..2a8375d018d 100644 --- a/packages/destination-actions/src/destinations/wingify/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/wingify/__tests__/index.test.ts @@ -20,7 +20,9 @@ describe('Wingify AccountID Validation', () => { wingifyAccountId: 65431231, region: 'US' } - await expect(testDestination.testAuthentication(settings)).rejects.toThrowError() + await expect(testDestination.testAuthentication(settings)).rejects.toThrow( + 'Invalid AccountID. Please check your AccountID' + ) }) }) }) diff --git a/packages/destination-actions/src/destinations/wingify/identifyUser/__tests__/index.test.ts b/packages/destination-actions/src/destinations/wingify/identifyUser/__tests__/index.test.ts index 5750205480e..18ab362f89e 100644 --- a/packages/destination-actions/src/destinations/wingify/identifyUser/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/wingify/identifyUser/__tests__/index.test.ts @@ -4,7 +4,7 @@ import Destination from '../../index' const testDestination = createTestIntegration(Destination) -const BASE_ENDPOINT = 'https://dev.visualwebsiteoptimizer.com' +const BASE_ENDPOINT = 'https://collect.wingify.net' const accountId = 654331 const wingifyUuid = 'ABC123' const SDK_KEY = 'sample-api-key' @@ -15,7 +15,7 @@ describe('Wingify.identifyUser Web', () => { it('should send segment traits as Wingify attributes', async () => { const event = createTestEvent({ traits: { - wingifyUuid: wingifyUuid, + wingify_uuid: wingifyUuid, textProperty: 'Hello' } }) @@ -74,7 +74,7 @@ describe('Wingify.identifyUser Web', () => { it('should send segment traits as Wingify attributes', async () => { const event = createTestEvent({ traits: { - wingifyUuid: wingifyUuid, + wingify_uuid: wingifyUuid, textProperty: 'Hello' }, context: { @@ -143,7 +143,7 @@ describe('Wingify.identifyUser Fullstack', () => { it('should send segment traits as Wingify attributes', async () => { const event = createTestEvent({ traits: { - wingifyUuid: wingifyUuid, + wingify_uuid: wingifyUuid, textProperty: 'Hello' } }) @@ -205,7 +205,7 @@ describe('Wingify.identifyUser Fullstack', () => { it('should send segment traits as Wingify attributes', async () => { const event = createTestEvent({ traits: { - wingifyUuid: wingifyUuid, + wingify_uuid: wingifyUuid, textProperty: 'Hello' }, context: { diff --git a/packages/destination-actions/src/destinations/wingify/identifyUser/generated-types.ts b/packages/destination-actions/src/destinations/wingify/identifyUser/generated-types.ts index ba88835eaf2..69ad36adbcc 100644 --- a/packages/destination-actions/src/destinations/wingify/identifyUser/generated-types.ts +++ b/packages/destination-actions/src/destinations/wingify/identifyUser/generated-types.ts @@ -18,7 +18,7 @@ export interface Payload { [k: string]: unknown } /** - * IP address of the user + * IP address of the user. Only useful when events originate from Segment client libraries (web/mobile); server-side events will contain Segment server IPs. */ ip?: string /** diff --git a/packages/destination-actions/src/destinations/wingify/identifyUser/index.ts b/packages/destination-actions/src/destinations/wingify/identifyUser/index.ts index b80f6c99f3b..080ea68f1a9 100644 --- a/packages/destination-actions/src/destinations/wingify/identifyUser/index.ts +++ b/packages/destination-actions/src/destinations/wingify/identifyUser/index.ts @@ -1,7 +1,7 @@ import { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import {formatPayload, formatAttributes, hosts} from '../utility' +import { formatPayload, formatAttributes, hosts } from '../utility' const action: ActionDefinition = { title: 'Identify User', @@ -36,7 +36,8 @@ const action: ActionDefinition = { } }, ip: { - description: 'IP address of the user', + description: + 'IP address of the user. Only useful when events originate from Segment client libraries (web/mobile); server-side events will contain Segment server IPs.', label: 'IP Address', required: false, type: 'string', @@ -86,7 +87,7 @@ const action: ActionDefinition = { structuredPayload.d.visitor = visitor structuredPayload.d.event.props.$visitor = visitor } - const region = settings.region || "US" + const region = settings.region || 'US' const host = hosts[region] const endpoint = `${host}/events/t?en=${eventName}&a=${settings.wingifyAccountId}` return request(endpoint, { diff --git a/packages/destination-actions/src/destinations/wingify/index.ts b/packages/destination-actions/src/destinations/wingify/index.ts index 879125e0789..a0bcddccbdd 100644 --- a/packages/destination-actions/src/destinations/wingify/index.ts +++ b/packages/destination-actions/src/destinations/wingify/index.ts @@ -1,6 +1,6 @@ import type { DestinationDefinition } from '@segment/actions-core' import type { Settings } from './generated-types' -import { defaultValues } from '@segment/actions-core' +import { defaultValues, InvalidAuthenticationError } from '@segment/actions-core' import pageVisit from './pageVisit' import trackEvent from './trackEvent' @@ -35,7 +35,7 @@ const destination: DestinationDefinition = { }, { name: 'Sync Audience', - subscribe: 'event = "Audience Entered" or event = "Audience Exited"', + subscribe: 'type = "track" or type = "identify"', partnerAction: 'syncAudience', mapping: defaultValues(syncAudience.fields), type: 'automatic' @@ -44,7 +44,7 @@ const destination: DestinationDefinition = { authentication: { scheme: 'custom', fields: { - vwoAccountId: { + wingifyAccountId: { label: 'Your Wingify account ID', description: 'Enter your Wingify Account ID', type: 'number', @@ -70,7 +70,7 @@ const destination: DestinationDefinition = { }, testAuthentication: (_request, { settings }) => { if (settings.wingifyAccountId < 1 || settings.wingifyAccountId.toString().length > 7) { - throw new Error('Invalid AccountID. Please check your AccountID') + throw new InvalidAuthenticationError('Invalid AccountID. Please check your AccountID') } else { return true } diff --git a/packages/destination-actions/src/destinations/wingify/metadata.json b/packages/destination-actions/src/destinations/wingify/metadata.json new file mode 100644 index 00000000000..fbd07c5c7d0 --- /dev/null +++ b/packages/destination-actions/src/destinations/wingify/metadata.json @@ -0,0 +1,833 @@ +{ + "slug": "actions-wingify-cloud", + "name": "Wingify Cloud Mode (Actions)", + "mode": "cloud", + "authentication": { + "scheme": "custom", + "fields": { + "wingifyAccountId": { + "label": "Your Wingify account ID", + "description": "Enter your Wingify Account ID", + "type": "number", + "required": true, + "multiple": false, + "choices": null, + "default": null, + "depends_on": null + }, + "apikey": { + "label": "Wingify SDK Key", + "description": "Wingify Fullstack SDK Key. It is mandatory when using the Wingify Fullstack suite.", + "type": "password", + "required": false, + "multiple": false, + "choices": null, + "default": null, + "depends_on": null + }, + "region": { + "label": "Region", + "description": "Wingify Region to sync data to. Default is US", + "type": "string", + "required": false, + "multiple": false, + "choices": [ + { + "label": "US", + "value": "US" + }, + { + "label": "Europe", + "value": "EU" + }, + { + "label": "Asia", + "value": "AS" + } + ], + "default": "US", + "depends_on": null + } + } + }, + "audienceConfig": null, + "actions": { + "trackEvent": { + "title": "Track Event", + "description": "Sends Segment's track event to Wingify", + "platform": "cloud", + "defaultSubscription": "type = \"track\"", + "hidden": false, + "hasPerformBatch": false, + "syncMode": null, + "hooks": null, + "dynamicFields": null, + "fields": { + "name": { + "label": "Name", + "description": "Name of the event", + "type": "string", + "required": true, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.event" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "properties": { + "label": "Properties", + "description": "JSON object containing additional properties that will be associated with the event.", + "type": "object", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.properties" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "wingifyUuid": { + "label": "Wingify UUID", + "description": "Wingify UUID", + "type": "string", + "required": true, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.properties.wingify_uuid" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "page": { + "label": "Page", + "description": "Contains context information regarding a webpage", + "type": "object", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.context.page" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "ip": { + "label": "IP Address", + "description": "IP address of the user. Only useful when events originate from Segment client libraries (web/mobile); server-side events will contain Segment server IPs.", + "type": "string", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.context.ip" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "userAgent": { + "label": "User Agent", + "description": "User-Agent of the user", + "type": "string", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.context.userAgent" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "timestamp": { + "label": "Timestamp", + "description": "Timestamp on the event", + "type": "string", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.timestamp" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + } + } + }, + "identifyUser": { + "title": "Identify User", + "description": "Maps Segment's visitor traits to the visitor attributes in Wingify", + "platform": "cloud", + "defaultSubscription": "type = \"identify\"", + "hidden": false, + "hasPerformBatch": false, + "syncMode": null, + "hooks": null, + "dynamicFields": null, + "fields": { + "attributes": { + "label": "attributes", + "description": "Visitor's attributes to be mapped", + "type": "object", + "required": true, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.traits" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "wingifyUuid": { + "label": "Wingify UUID", + "description": "Wingify UUID", + "type": "string", + "required": true, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.traits.wingify_uuid" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "page": { + "label": "Page", + "description": "Contains context information regarding a webpage", + "type": "object", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.context.page" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "ip": { + "label": "IP Address", + "description": "IP address of the user. Only useful when events originate from Segment client libraries (web/mobile); server-side events will contain Segment server IPs.", + "type": "string", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.context.ip" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "userAgent": { + "label": "User Agent", + "description": "User-Agent of the user", + "type": "string", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.context.userAgent" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "timestamp": { + "label": "Timestamp", + "description": "Timestamp on the event", + "type": "string", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.timestamp" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + } + } + }, + "pageVisit": { + "title": "Page Visit", + "description": "Sends Segment's page event to Wingify", + "platform": "cloud", + "defaultSubscription": "type = \"page\"", + "hidden": false, + "hasPerformBatch": false, + "syncMode": null, + "hooks": null, + "dynamicFields": null, + "fields": { + "url": { + "label": "Page URL", + "description": "URL of the webpage", + "type": "string", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.context.page.url" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "wingifyUuid": { + "label": "Wingify UUID", + "description": "Wingify UUID", + "type": "string", + "required": true, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.properties.wingify_uuid" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "page": { + "label": "Page", + "description": "Contains context information regarding a webpage", + "type": "object", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.context.page" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "ip": { + "label": "IP Address", + "description": "IP address of the user. Only useful when events originate from Segment client libraries (web/mobile); server-side events will contain Segment server IPs.", + "type": "string", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.context.ip" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "userAgent": { + "label": "User Agent", + "description": "User-Agent of the user", + "type": "string", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.context.userAgent" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "timestamp": { + "label": "Timestamp", + "description": "Timestamp on the event", + "type": "string", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.timestamp" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + } + } + }, + "syncAudience": { + "title": "Sync Audience", + "description": "Syncs Segment audiences to Wingify", + "platform": "cloud", + "defaultSubscription": "type = \"track\" or type = \"identify\"", + "hidden": false, + "hasPerformBatch": false, + "syncMode": null, + "hooks": null, + "dynamicFields": null, + "fields": { + "name": { + "label": "Event Name", + "description": "Name of the event", + "type": "string", + "required": true, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.event" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "userId": { + "label": "User ID", + "description": "An unique identifier for the user", + "type": "string", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.userId" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "anonymousId": { + "label": "Anonymous ID", + "description": "Anonymous ID for users", + "type": "string", + "required": false, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@path": "$.anonymousId" + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + }, + "audienceId": { + "label": "Audience ID", + "description": "Segment's audience ID", + "type": "string", + "required": true, + "multiple": false, + "allowNull": false, + "dynamic": false, + "default": { + "@if": { + "exists": { + "@path": "$.context.personas.computation_key" + }, + "then": { + "@path": "$.context.personas.computation_key" + }, + "else": { + "@path": "$.properties.audience_key" + } + } + }, + "choices": null, + "placeholder": null, + "properties": null, + "category": null, + "depends_on": null, + "readOnly": null, + "hidden": null, + "minimum": null, + "maximum": null, + "defaultObjectUI": null, + "disabledInputMethods": null, + "displayMode": null, + "format": null, + "additionalProperties": false + } + } + } + }, + "presets": [ + { + "name": "Track Event", + "type": "automatic", + "partnerAction": "trackEvent", + "subscribe": "type = \"track\"", + "mapping": { + "name": { + "@path": "$.event" + }, + "properties": { + "@path": "$.properties" + }, + "wingifyUuid": { + "@path": "$.properties.wingify_uuid" + }, + "page": { + "@path": "$.context.page" + }, + "ip": { + "@path": "$.context.ip" + }, + "userAgent": { + "@path": "$.context.userAgent" + }, + "timestamp": { + "@path": "$.timestamp" + } + }, + "eventSlug": null + }, + { + "name": "Identify User", + "type": "automatic", + "partnerAction": "identifyUser", + "subscribe": "type = \"identify\"", + "mapping": { + "attributes": { + "@path": "$.traits" + }, + "wingifyUuid": { + "@path": "$.traits.wingify_uuid" + }, + "page": { + "@path": "$.context.page" + }, + "ip": { + "@path": "$.context.ip" + }, + "userAgent": { + "@path": "$.context.userAgent" + }, + "timestamp": { + "@path": "$.timestamp" + } + }, + "eventSlug": null + }, + { + "name": "Page Visit", + "type": "automatic", + "partnerAction": "pageVisit", + "subscribe": "type = \"page\"", + "mapping": { + "url": { + "@path": "$.context.page.url" + }, + "wingifyUuid": { + "@path": "$.properties.wingify_uuid" + }, + "page": { + "@path": "$.context.page" + }, + "ip": { + "@path": "$.context.ip" + }, + "userAgent": { + "@path": "$.context.userAgent" + }, + "timestamp": { + "@path": "$.timestamp" + } + }, + "eventSlug": null + }, + { + "name": "Sync Audience", + "type": "automatic", + "partnerAction": "syncAudience", + "subscribe": "type = \"track\" or type = \"identify\"", + "mapping": { + "name": { + "@path": "$.event" + }, + "userId": { + "@path": "$.userId" + }, + "anonymousId": { + "@path": "$.anonymousId" + }, + "audienceId": { + "@if": { + "exists": { + "@path": "$.context.personas.computation_key" + }, + "then": { + "@path": "$.context.personas.computation_key" + }, + "else": { + "@path": "$.properties.audience_key" + } + } + } + }, + "eventSlug": null + } + ] +} diff --git a/packages/destination-actions/src/destinations/wingify/pageVisit/__tests__/index.test.ts b/packages/destination-actions/src/destinations/wingify/pageVisit/__tests__/index.test.ts index c72f63825bb..db1feb7eb27 100644 --- a/packages/destination-actions/src/destinations/wingify/pageVisit/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/wingify/pageVisit/__tests__/index.test.ts @@ -4,7 +4,7 @@ import Destination from '../../index' const testDestination = createTestIntegration(Destination) -const BASE_ENDPOINT = 'https://dev.visualwebsiteoptimizer.com' +const BASE_ENDPOINT = 'https://collect.wingify.net' const accountId = 654331 const wingifyUuid = 'ABC123' const EVENT_NAME = 'segment.pageView' @@ -16,7 +16,7 @@ describe('Wingify.pageVisit Web', () => { it('should send Page Visit event to Wingify', async () => { const event = createTestEvent({ properties: { - wingifyUuid: wingifyUuid + wingify_uuid: wingifyUuid } }) nock(BASE_ENDPOINT).post(`/events/t?en=${EVENT_NAME}&a=${accountId}`).reply(200, {}) @@ -65,7 +65,7 @@ describe('Wingify.pageVisit Web', () => { it('should send Page Visit event to Wingify', async () => { const event = createTestEvent({ properties: { - wingifyUuid: wingifyUuid + wingify_uuid: wingifyUuid }, context: { page: { @@ -124,7 +124,7 @@ describe('Wingify.pageVisit Fullstack', () => { it('should send Page Visit event to Wingify', async () => { const event = createTestEvent({ properties: { - wingifyUuid: wingifyUuid + wingify_uuid: wingifyUuid } }) nock(BASE_ENDPOINT).post(`/events/t?en=${EVENT_NAME}&a=${accountId}`).reply(200, {}) @@ -184,7 +184,7 @@ describe('Wingify.pageVisit Fullstack', () => { it('should send Page Visit event to Wingify', async () => { const event = createTestEvent({ properties: { - wingifyUuid: wingifyUuid + wingify_uuid: wingifyUuid }, context: { page: { diff --git a/packages/destination-actions/src/destinations/wingify/pageVisit/generated-types.ts b/packages/destination-actions/src/destinations/wingify/pageVisit/generated-types.ts index 9fab9c51279..aa91743b18f 100644 --- a/packages/destination-actions/src/destinations/wingify/pageVisit/generated-types.ts +++ b/packages/destination-actions/src/destinations/wingify/pageVisit/generated-types.ts @@ -16,7 +16,7 @@ export interface Payload { [k: string]: unknown } /** - * IP address of the user + * IP address of the user. Only useful when events originate from Segment client libraries (web/mobile); server-side events will contain Segment server IPs. */ ip?: string /** diff --git a/packages/destination-actions/src/destinations/wingify/pageVisit/index.ts b/packages/destination-actions/src/destinations/wingify/pageVisit/index.ts index f87fe56967f..70f5e469cd6 100644 --- a/packages/destination-actions/src/destinations/wingify/pageVisit/index.ts +++ b/packages/destination-actions/src/destinations/wingify/pageVisit/index.ts @@ -35,7 +35,8 @@ const action: ActionDefinition = { } }, ip: { - description: 'IP address of the user', + description: + 'IP address of the user. Only useful when events originate from Segment client libraries (web/mobile); server-side events will contain Segment server IPs.', label: 'IP Address', required: false, type: 'string', diff --git a/packages/destination-actions/src/destinations/wingify/syncAudience/__tests__/index.test.ts b/packages/destination-actions/src/destinations/wingify/syncAudience/__tests__/index.test.ts index 4addab4e628..9f0c5977866 100644 --- a/packages/destination-actions/src/destinations/wingify/syncAudience/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/wingify/syncAudience/__tests__/index.test.ts @@ -4,24 +4,33 @@ import Destination from '../../index' const testDestination = createTestIntegration(Destination) -const BASE_ENDPOINT = 'https://dev.visualwebsiteoptimizer.com' -const accountId = 654331 +const BASE_ENDPOINT = 'https://collect.wingify.net' +const WINGIFY_ACCOUNT_ID = 654331 +const AUDIENCE_KEY = 'test_audience' describe('Wingify.syncAudience', () => { it('should send the add audience call', async () => { const event = createTestEvent({ - event: 'Audience Entered', + type: 'track', userId: 'test_user', + context: { + personas: { + computation_class: 'audience', + computation_key: AUDIENCE_KEY + } + }, properties: { - audience_key: 'test_audience' + audience_key: AUDIENCE_KEY, + [AUDIENCE_KEY]: true } }) - nock(BASE_ENDPOINT).post(`/events/t?en=wingify_integration&a=${accountId}`).reply(200, {}) + nock(BASE_ENDPOINT).post(`/events/t?en=wingify_integration&a=${WINGIFY_ACCOUNT_ID}`).reply(200, {}) const responses = await testDestination.testAction('syncAudience', { event, useDefaultMappings: true, + audienceMembership: true, settings: { - wingifyAccountId: accountId, + wingifyAccountId: WINGIFY_ACCOUNT_ID, apikey: '' } }) @@ -31,8 +40,8 @@ describe('Wingify.syncAudience', () => { name: 'wingify_integration', props: { action: 'audience_entered', - audienceName: 'test_audience', - audienceId: 'test_audience', + audienceName: AUDIENCE_KEY, + audienceId: AUDIENCE_KEY, identifier: 'test_user', accountId: 654331, integration: 'segment' @@ -46,19 +55,27 @@ describe('Wingify.syncAudience', () => { it('should handle anonymous users', async () => { const event = createTestEvent({ - event: 'Audience Entered', + type: 'track', userId: null, anonymousId: 'anonymous-id', + context: { + personas: { + computation_class: 'audience', + computation_key: AUDIENCE_KEY + } + }, properties: { - audience_key: 'test_audience' + audience_key: AUDIENCE_KEY, + [AUDIENCE_KEY]: true } }) - nock(BASE_ENDPOINT).post(`/events/t?en=wingify_integration&a=${accountId}`).reply(200, {}) + nock(BASE_ENDPOINT).post(`/events/t?en=wingify_integration&a=${WINGIFY_ACCOUNT_ID}`).reply(200, {}) const responses = await testDestination.testAction('syncAudience', { event, useDefaultMappings: true, + audienceMembership: true, settings: { - wingifyAccountId: accountId, + wingifyAccountId: WINGIFY_ACCOUNT_ID, apikey: '' } }) @@ -68,8 +85,8 @@ describe('Wingify.syncAudience', () => { name: 'wingify_integration', props: { action: 'audience_entered', - audienceName: 'test_audience', - audienceId: 'test_audience', + audienceName: AUDIENCE_KEY, + audienceId: AUDIENCE_KEY, identifier: 'anonymous-id', accountId: 654331, integration: 'segment' @@ -83,18 +100,26 @@ describe('Wingify.syncAudience', () => { it('should send the remove audience call', async () => { const event = createTestEvent({ - event: 'Audience Exited', + type: 'track', userId: 'test_user', + context: { + personas: { + computation_class: 'audience', + computation_key: AUDIENCE_KEY + } + }, properties: { - audience_key: 'test_audience' + audience_key: AUDIENCE_KEY, + [AUDIENCE_KEY]: false } }) - nock(BASE_ENDPOINT).post(`/events/t?en=wingify_integration&a=${accountId}`).reply(200, {}) + nock(BASE_ENDPOINT).post(`/events/t?en=wingify_integration&a=${WINGIFY_ACCOUNT_ID}`).reply(200, {}) const responses = await testDestination.testAction('syncAudience', { event, useDefaultMappings: true, + audienceMembership: false, settings: { - wingifyAccountId: accountId, + wingifyAccountId: WINGIFY_ACCOUNT_ID, apikey: '' } }) @@ -104,8 +129,8 @@ describe('Wingify.syncAudience', () => { name: 'wingify_integration', props: { action: 'audience_exited', - audienceName: 'test_audience', - audienceId: 'test_audience', + audienceName: AUDIENCE_KEY, + audienceId: AUDIENCE_KEY, identifier: 'test_user', accountId: 654331, integration: 'segment' diff --git a/packages/destination-actions/src/destinations/wingify/syncAudience/index.ts b/packages/destination-actions/src/destinations/wingify/syncAudience/index.ts index 71382144216..a3e607d96cf 100644 --- a/packages/destination-actions/src/destinations/wingify/syncAudience/index.ts +++ b/packages/destination-actions/src/destinations/wingify/syncAudience/index.ts @@ -1,12 +1,12 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import {hosts} from "../utility"; +import { hosts } from '../utility' const action: ActionDefinition = { title: 'Sync Audience', description: 'Syncs Segment audiences to Wingify', - defaultSubscription: 'event = "Audience Entered" or event = "Audience Exited"', + defaultSubscription: 'type = "track" or type = "identify"', fields: { name: { description: 'Name of the event', @@ -39,19 +39,18 @@ const action: ActionDefinition = { required: true, type: 'string', default: { - '@path': '$.properties.audience_key' + '@if': { + exists: { '@path': '$.context.personas.computation_key' }, + then: { '@path': '$.context.personas.computation_key' }, + else: { '@path': '$.properties.audience_key' } + } } } }, - perform: (request, { settings, payload }) => { + perform: async (request, { settings, payload, audienceMembership }) => { const epochTime = new Date().valueOf() const time = Math.floor(epochTime) - let action - if (payload.name == 'Audience Entered') { - action = 'audience_entered' - } else if (payload.name == 'Audience Exited') { - action = 'audience_exited' - } + const action = audienceMembership ? 'audience_entered' : 'audience_exited' const wingifyPayload = { d: { event: { @@ -68,7 +67,7 @@ const action: ActionDefinition = { } } } - const region = settings.region || "US" + const region = settings.region || 'US' const host = hosts[region] const endpoint = `${host}/events/t?en=wingify_integration&a=${settings.wingifyAccountId}` diff --git a/packages/destination-actions/src/destinations/wingify/trackEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/wingify/trackEvent/__tests__/index.test.ts index 3897025ebee..5316b7fab36 100644 --- a/packages/destination-actions/src/destinations/wingify/trackEvent/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/wingify/trackEvent/__tests__/index.test.ts @@ -4,7 +4,7 @@ import Destination from '../../index' const testDestination = createTestIntegration(Destination) -const BASE_ENDPOINT = 'https://dev.visualwebsiteoptimizer.com' +const BASE_ENDPOINT = 'https://collect.wingify.net' const accountId = 654331 const wingifyUuid = 'ABC123' const SDK_KEY = 'sample-api-key' @@ -16,7 +16,7 @@ describe('Wingify.trackEvent Web', () => { const event = createTestEvent({ event: 'testEvent', properties: { - wingifyUuid: wingifyUuid + wingify_uuid: wingifyUuid } }) nock(BASE_ENDPOINT).post(`/events/t?en=segment.testEvent&a=${accountId}`).reply(200, {}) @@ -66,7 +66,7 @@ describe('Wingify.trackEvent Web', () => { const event = createTestEvent({ event: 'testEvent', properties: { - wingifyUuid: wingifyUuid, + wingify_uuid: wingifyUuid, amount: 100, currency: 'INR', outbound: true @@ -111,7 +111,7 @@ describe('Wingify.trackEvent Web', () => { const event = createTestEvent({ event: 'testEvent', properties: { - wingifyUuid: wingifyUuid + wingify_uuid: wingifyUuid }, context: { page: { @@ -169,7 +169,7 @@ describe('Wingify.trackEvent Web', () => { const event = createTestEvent({ event: 'testEvent', properties: { - wingifyUuid: wingifyUuid, + wingify_uuid: wingifyUuid, amount: 100, currency: 'INR', outbound: true @@ -224,7 +224,7 @@ describe('Wingify.trackEvent Fullstack', () => { const event = createTestEvent({ event: 'testEvent', properties: { - wingifyUuid: wingifyUuid + wingify_uuid: wingifyUuid } }) nock(BASE_ENDPOINT).post(`/events/t?en=segment.testEvent&a=${accountId}`).reply(200, {}) @@ -284,7 +284,7 @@ describe('Wingify.trackEvent Fullstack', () => { const event = createTestEvent({ event: 'testEvent', properties: { - wingifyUuid: wingifyUuid, + wingify_uuid: wingifyUuid, amount: 100, currency: 'INR', outbound: true @@ -340,7 +340,7 @@ describe('Wingify.trackEvent Fullstack', () => { const event = createTestEvent({ event: 'testEvent', properties: { - wingifyUuid: wingifyUuid + wingify_uuid: wingifyUuid }, context: { page: { @@ -408,7 +408,7 @@ describe('Wingify.trackEvent Fullstack', () => { const event = createTestEvent({ event: 'testEvent', properties: { - wingifyUuid: wingifyUuid, + wingify_uuid: wingifyUuid, amount: 100, currency: 'INR', outbound: true diff --git a/packages/destination-actions/src/destinations/wingify/trackEvent/generated-types.ts b/packages/destination-actions/src/destinations/wingify/trackEvent/generated-types.ts index e5ec3a1b812..a93e9e6081c 100644 --- a/packages/destination-actions/src/destinations/wingify/trackEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/wingify/trackEvent/generated-types.ts @@ -22,7 +22,7 @@ export interface Payload { [k: string]: unknown } /** - * IP address of the user + * IP address of the user. Only useful when events originate from Segment client libraries (web/mobile); server-side events will contain Segment server IPs. */ ip?: string /** diff --git a/packages/destination-actions/src/destinations/wingify/trackEvent/index.ts b/packages/destination-actions/src/destinations/wingify/trackEvent/index.ts index 2461457e843..903aefa3053 100644 --- a/packages/destination-actions/src/destinations/wingify/trackEvent/index.ts +++ b/packages/destination-actions/src/destinations/wingify/trackEvent/index.ts @@ -45,7 +45,8 @@ const action: ActionDefinition = { } }, ip: { - description: 'IP address of the user', + description: + 'IP address of the user. Only useful when events originate from Segment client libraries (web/mobile); server-side events will contain Segment server IPs.', label: 'IP Address', required: false, type: 'string', @@ -83,7 +84,7 @@ const action: ActionDefinition = { settings.wingifyAccountId ) structuredPayload.d.event.props.wingifyMeta['ogName'] = payload.name - const region = settings.region || "US" + const region = settings.region || 'US' const host = hosts[region] const endpoint = `${host}/events/t?en=${sanitisedEventName}&a=${settings.wingifyAccountId}` return request(endpoint, { diff --git a/packages/destination-actions/src/destinations/wingify/utility.ts b/packages/destination-actions/src/destinations/wingify/utility.ts index 82eedddab7c..1ec105b03e7 100644 --- a/packages/destination-actions/src/destinations/wingify/utility.ts +++ b/packages/destination-actions/src/destinations/wingify/utility.ts @@ -4,9 +4,9 @@ import * as crypto from 'crypto' const namespace = '11e13cd7-6c48-53ec-8679-7e9c752273c5' export const hosts: { [key: string]: string } = { - US: 'https://eadge.wingify.com', - EU: 'https://eadge.wingify.com/eu01', - AS: 'https://eadge.wingify.com/as01' + US: 'https://collect.wingify.net', + EU: 'https://collect.wingify.net/eu01', + AS: 'https://collect.wingify.net/as01' } function uuidv5(name: string, namespace: string): string { @@ -105,6 +105,9 @@ export function sanitiseEventName(name: string) { export function formatAttributes(attributes: { [k: string]: unknown } | undefined) { const formattedAttributes: { [k: string]: unknown } = {} + if (!attributes) { + return formattedAttributes + } for (const key in attributes) { formattedAttributes[`segment.${key}`] = attributes[key] }