Skip to content

Commit 90b332a

Browse files
STRATCONN-6465 - [Appcues web] - new destination (#3568)
* STRATCONN-6465 - [Appcues web] - new destination * Update packages/browser-destinations/destinations/appcues-web/src/identify/index.ts Co-authored-by: Copilot <[email protected]> * Update packages/browser-destinations/destinations/appcues-web/src/index.ts Co-authored-by: Copilot <[email protected]> * Update packages/browser-destinations/destinations/appcues-web/src/group/index.ts Co-authored-by: Copilot <[email protected]> * Update packages/browser-destinations/destinations/appcues-web/src/index.ts Co-authored-by: Copilot <[email protected]> * Update packages/browser-destinations/destinations/appcues-web/src/index.ts Co-authored-by: Copilot <[email protected]> * handle arrays and objects * post review * yarn types * removing accidental commits --------- Co-authored-by: Copilot <[email protected]>
1 parent 04efab7 commit 90b332a

20 files changed

Lines changed: 845 additions & 0 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# @segment/analytics-browser-appcues-web-actions
2+
3+
The Appcues Web browser action destination for use with @segment/analytics-next.
4+
5+
## License
6+
7+
MIT License
8+
9+
Copyright (c) 2025 Segment
10+
11+
Permission is hereby granted, free of charge, to any person obtaining a copy
12+
of this software and associated documentation files (the "Software"), to deal
13+
in the Software without restriction, including without limitation the rights
14+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15+
copies of the Software, and to permit persons to whom the Software is
16+
furnished to do so, subject to the following conditions:
17+
18+
The above copyright notice and this permission notice shall be included in all
19+
copies or substantial portions of the Software.
20+
21+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27+
SOFTWARE.
28+
29+
## Contributing
30+
31+
All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "@segment/analytics-browser-appcues-web-actions",
3+
"version": "1.0.0",
4+
"license": "MIT",
5+
"publishConfig": {
6+
"access": "public",
7+
"registry": "https://registry.npmjs.org"
8+
},
9+
"main": "./dist/cjs",
10+
"module": "./dist/esm",
11+
"scripts": {
12+
"build": "yarn build:esm && yarn build:cjs",
13+
"build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
14+
"build:esm": "tsc --outDir ./dist/esm"
15+
},
16+
"typings": "./dist/esm",
17+
"dependencies": {
18+
"@segment/browser-destination-runtime": "^1.93.0"
19+
},
20+
"peerDependencies": {
21+
"@segment/analytics-next": ">=1.55.0"
22+
}
23+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Analytics, Context } from '@segment/analytics-next'
2+
import AppcuesDestination, { destination } from '../index'
3+
import { Appcues } from '../types'
4+
5+
describe('Appcues Web initialization', () => {
6+
const baseSettings = {
7+
accountID: 'test-account-id',
8+
region: 'US' as const,
9+
enableURLDetection: true
10+
}
11+
12+
const subscriptions = [
13+
{
14+
partnerAction: 'track',
15+
name: 'Track',
16+
enabled: true,
17+
subscribe: 'type = "track"',
18+
mapping: {
19+
event: { '@path': '$.event' },
20+
properties: { '@path': '$.properties' }
21+
}
22+
}
23+
]
24+
25+
afterEach(() => {
26+
jest.restoreAllMocks()
27+
delete (window as any).Appcues
28+
delete (window as any).AppcuesSettings
29+
})
30+
31+
test('initialize loads script and resolves with Appcues instance', async () => {
32+
const mockAppcuesInstance: Appcues = {
33+
track: jest.fn(),
34+
identify: jest.fn(),
35+
group: jest.fn(),
36+
page: jest.fn()
37+
}
38+
39+
const mockLoadScript = jest.fn().mockResolvedValue(undefined)
40+
const mockResolveWhen = jest.fn().mockResolvedValue(undefined)
41+
42+
jest.spyOn(destination, 'initialize').mockImplementation(async ({ settings }, deps) => {
43+
await mockLoadScript()
44+
;(window as any).Appcues = mockAppcuesInstance
45+
await mockResolveWhen()
46+
return mockAppcuesInstance
47+
})
48+
49+
const [event] = await AppcuesDestination({
50+
...baseSettings,
51+
subscriptions
52+
})
53+
54+
await event.load(Context.system(), {} as Analytics)
55+
56+
expect(destination.initialize).toHaveBeenCalled()
57+
})
58+
59+
test('initialize sets AppcuesSettings with enableURLDetection', async () => {
60+
const mockAppcuesInstance: Appcues = {
61+
track: jest.fn(),
62+
identify: jest.fn(),
63+
group: jest.fn(),
64+
page: jest.fn()
65+
}
66+
67+
jest.spyOn(destination, 'initialize').mockImplementation(async ({ settings }) => {
68+
;(window as any).AppcuesSettings = { enableURLDetection: settings.enableURLDetection }
69+
;(window as any).Appcues = mockAppcuesInstance
70+
return mockAppcuesInstance
71+
})
72+
73+
const [event] = await AppcuesDestination({
74+
...baseSettings,
75+
enableURLDetection: false,
76+
subscriptions
77+
})
78+
79+
await event.load(Context.system(), {} as Analytics)
80+
81+
expect((window as any).AppcuesSettings).toEqual({ enableURLDetection: false })
82+
})
83+
})
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const URL = {
2+
US: "fast.appcues.com",
3+
EU: "fast.eu.appcues.com"
4+
}

packages/browser-destinations/destinations/appcues-web/src/generated-types.ts

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { Analytics, Context } from '@segment/analytics-next'
2+
import { Subscription } from '@segment/browser-destination-runtime'
3+
import AppcuesDestination, { destination } from '../../index'
4+
import { Appcues } from '../../types'
5+
6+
describe('Appcues.group', () => {
7+
const settings = {
8+
accountID: 'test-account-id',
9+
region: 'US' as const,
10+
enableURLDetection: true
11+
}
12+
13+
let mockAppcues: Appcues
14+
let event: any
15+
16+
beforeEach(async () => {
17+
jest.restoreAllMocks()
18+
jest.spyOn(destination, 'initialize').mockImplementation(() => {
19+
mockAppcues = {
20+
track: jest.fn(),
21+
identify: jest.fn(),
22+
group: jest.fn(),
23+
page: jest.fn()
24+
}
25+
return Promise.resolve(mockAppcues)
26+
})
27+
})
28+
29+
test('group() handled correctly', async () => {
30+
const subscriptions: Subscription[] = [
31+
{
32+
partnerAction: 'group',
33+
name: 'Group',
34+
enabled: true,
35+
subscribe: 'type = "group"',
36+
mapping: {
37+
groupId: { '@path': '$.groupId' },
38+
traits: { '@path': '$.traits' }
39+
}
40+
}
41+
]
42+
43+
const context = new Context({
44+
messageId: 'ajs-test-message-id',
45+
type: 'group',
46+
anonymousId: 'anonymous-id-123',
47+
userId: 'user-123',
48+
groupId: 'group-456',
49+
traits: {
50+
name: 'Acme Corp',
51+
plan: 'enterprise',
52+
employees: 100
53+
}
54+
})
55+
56+
const [groupEvent] = await AppcuesDestination({
57+
...settings,
58+
subscriptions
59+
})
60+
event = groupEvent
61+
62+
await event.load(Context.system(), {} as Analytics)
63+
await event.group?.(context)
64+
65+
expect(mockAppcues.group).toHaveBeenCalledWith('group-456', {
66+
name: 'Acme Corp',
67+
plan: 'enterprise',
68+
employees: 100
69+
})
70+
})
71+
72+
test('group() handles nested traits and arrays', async () => {
73+
const subscriptions: Subscription[] = [
74+
{
75+
partnerAction: 'group',
76+
name: 'Group',
77+
enabled: true,
78+
subscribe: 'type = "group"',
79+
mapping: {
80+
groupId: { '@path': '$.groupId' },
81+
traits: { '@path': '$.traits' }
82+
}
83+
}
84+
]
85+
86+
const context = new Context({
87+
messageId: 'ajs-test-message-id',
88+
type: 'group',
89+
userId: 'user-123',
90+
groupId: 'group-456',
91+
traits: {
92+
name: 'Acme Corp',
93+
address: {
94+
city: 'New York',
95+
country: 'USA'
96+
},
97+
industries: ['tech', 'finance']
98+
}
99+
})
100+
101+
const [groupEvent] = await AppcuesDestination({
102+
...settings,
103+
subscriptions
104+
})
105+
event = groupEvent
106+
107+
await event.load(Context.system(), {} as Analytics)
108+
await event.group?.(context)
109+
110+
expect(mockAppcues.group).toHaveBeenCalledWith('group-456', {
111+
name: 'Acme Corp',
112+
address: {
113+
city: 'New York',
114+
country: 'USA'
115+
},
116+
industries: ['tech', 'finance']
117+
})
118+
})
119+
})

packages/browser-destinations/destinations/appcues-web/src/group/generated-types.ts

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types'
2+
import type { Settings } from '../generated-types'
3+
import type { Payload } from './generated-types'
4+
import type { Appcues } from '../types'
5+
6+
const action: BrowserActionDefinition<Settings, Appcues, Payload> = {
7+
title: 'Group',
8+
description: 'Send Segment group events to Appcues.',
9+
platform: 'web',
10+
fields: {
11+
groupId: {
12+
label: 'Group ID',
13+
description: 'The ID of the group to identify in Appcues.',
14+
required: true,
15+
type: 'string',
16+
default: { '@path': '$.groupId' }
17+
},
18+
traits: {
19+
label: 'Group traits',
20+
description: 'Properties to associate with the group / account / company.',
21+
required: false,
22+
type: 'object',
23+
default: { '@path': '$.traits' }
24+
}
25+
},
26+
defaultSubscription: 'type = "group"',
27+
perform: (appcues, { payload }) => {
28+
const { groupId, traits } = payload
29+
appcues.group(groupId, traits)
30+
}
31+
}
32+
33+
export default action

0 commit comments

Comments
 (0)