@segment/analytics-browser-actions-fullstory 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@segment/analytics-browser-actions-fullstory",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "main": "./dist/cjs",
6
+ "module": "./dist/esm",
7
+ "scripts": {
8
+ "build": "yarn build:esm && yarn build:cjs",
9
+ "build:cjs": "tsc --module commonjs --outDir ./dist/cjs",
10
+ "build:esm": "tsc --outDir ./dist/esm"
11
+ },
12
+ "typings": "./dist/esm",
13
+ "dependencies": {
14
+ "@fullstory/browser": "^1.4.9",
15
+ "@segment/actions-core": "^3.71.0",
16
+ "@segment/browser-destination-runtime": "^1.0.0"
17
+ },
18
+ "peerDependencies": {
19
+ "@segment/analytics-next": "*"
20
+ }
21
+ }
@@ -0,0 +1,205 @@
1
+ import { Analytics, Context } from '@segment/analytics-next'
2
+ import fullstory, { destination } from '..'
3
+ import { Subscription } from '@segment/browser-destination-runtime/types'
4
+
5
+ const example: Subscription[] = [
6
+ {
7
+ partnerAction: 'trackEvent',
8
+ name: 'Track Event',
9
+ enabled: true,
10
+ subscribe: 'type = "track"',
11
+ mapping: {
12
+ name: {
13
+ '@path': '$.name'
14
+ },
15
+ properties: {
16
+ '@path': '$.properties'
17
+ }
18
+ }
19
+ },
20
+ {
21
+ partnerAction: 'identifyUser',
22
+ name: 'Identify User',
23
+ enabled: true,
24
+ subscribe: 'type = "identify"',
25
+ mapping: {
26
+ anonymousId: {
27
+ '@path': '$.anonymousId'
28
+ },
29
+ userId: {
30
+ '@path': '$.userId'
31
+ },
32
+ email: {
33
+ '@path': '$.traits.email'
34
+ },
35
+ traits: {
36
+ '@path': '$.traits'
37
+ },
38
+ displayName: {
39
+ '@path': '$.traits.name'
40
+ }
41
+ }
42
+ }
43
+ ]
44
+
45
+ test('can load fullstory', async () => {
46
+ const [event] = await fullstory({
47
+ orgId: 'thefullstory.com',
48
+ subscriptions: example
49
+ })
50
+
51
+ jest.spyOn(destination.actions.trackEvent, 'perform')
52
+ jest.spyOn(destination, 'initialize')
53
+
54
+ await event.load(Context.system(), {} as Analytics)
55
+ expect(destination.initialize).toHaveBeenCalled()
56
+
57
+ const ctx = await event.track?.(
58
+ new Context({
59
+ type: 'track',
60
+ properties: {
61
+ banana: '📞'
62
+ }
63
+ })
64
+ )
65
+
66
+ expect(destination.actions.trackEvent.perform).toHaveBeenCalled()
67
+ expect(ctx).not.toBeUndefined()
68
+
69
+ const scripts = window.document.querySelectorAll('script')
70
+ expect(scripts).toMatchInlineSnapshot(`
71
+ NodeList [
72
+ <script
73
+ crossorigin="anonymous"
74
+ src="https://edge.fullstory.com/s/fs.js"
75
+ />,
76
+ <script>
77
+ // the emptiness
78
+ </script>,
79
+ ]
80
+ `)
81
+ })
82
+
83
+ describe('#track', () => {
84
+ it('sends record events to fullstory on "event"', async () => {
85
+ const [event] = await fullstory({
86
+ orgId: 'thefullstory.com',
87
+ subscriptions: example
88
+ })
89
+
90
+ await event.load(Context.system(), {} as Analytics)
91
+ const fs = jest.spyOn(window.FS, 'event')
92
+
93
+ await event.track?.(
94
+ new Context({
95
+ type: 'track',
96
+ name: 'hello!',
97
+ properties: {
98
+ banana: '📞'
99
+ }
100
+ })
101
+ )
102
+
103
+ expect(fs).toHaveBeenCalledWith(
104
+ 'hello!',
105
+ {
106
+ banana: '📞'
107
+ },
108
+ 'segment-browser-actions'
109
+ )
110
+ })
111
+ })
112
+
113
+ describe('#identify', () => {
114
+ it('should default to anonymousId', async () => {
115
+ const [_, identifyUser] = await fullstory({
116
+ orgId: 'thefullstory.com',
117
+ subscriptions: example
118
+ })
119
+
120
+ await identifyUser.load(Context.system(), {} as Analytics)
121
+ const fs = jest.spyOn(window.FS, 'setUserVars')
122
+ const fsId = jest.spyOn(window.FS, 'identify')
123
+
124
+ await identifyUser.identify?.(
125
+ new Context({
126
+ type: 'identify',
127
+ anonymousId: 'anon',
128
+ traits: {
129
+ testProp: false
130
+ }
131
+ })
132
+ )
133
+
134
+ expect(fs).toHaveBeenCalled()
135
+ expect(fsId).not.toHaveBeenCalled()
136
+ expect(fs).toHaveBeenCalledWith({ segmentAnonymousId_str: 'anon', testProp: false }, 'segment-browser-actions')
137
+ }),
138
+ it('should send an id', async () => {
139
+ const [_, identifyUser] = await fullstory({
140
+ orgId: 'thefullstory.com',
141
+ subscriptions: example
142
+ })
143
+ await identifyUser.load(Context.system(), {} as Analytics)
144
+ const fsId = jest.spyOn(window.FS, 'identify')
145
+
146
+ await identifyUser.identify?.(new Context({ type: 'identify', userId: 'id' }))
147
+ expect(fsId).toHaveBeenCalledWith('id', {}, 'segment-browser-actions')
148
+ }),
149
+ it('should camelCase custom traits', async () => {
150
+ const [_, identifyUser] = await fullstory({
151
+ orgId: 'thefullstory.com',
152
+ subscriptions: example
153
+ })
154
+ await identifyUser.load(Context.system(), {} as Analytics)
155
+ const fsId = jest.spyOn(window.FS, 'identify')
156
+
157
+ await identifyUser.identify?.(
158
+ new Context({
159
+ type: 'identify',
160
+ userId: 'id',
161
+ traits: {
162
+ 'not-cameled': false,
163
+ 'first name': 'John',
164
+ lastName: 'Doe'
165
+ }
166
+ })
167
+ )
168
+ expect(fsId).toHaveBeenCalledWith(
169
+ 'id',
170
+ { notCameled: false, firstName: 'John', lastName: 'Doe' },
171
+ 'segment-browser-actions'
172
+ )
173
+ })
174
+
175
+ it('can set user vars', async () => {
176
+ const [_, identifyUser] = await fullstory({
177
+ orgId: 'thefullstory.com',
178
+ subscriptions: example
179
+ })
180
+
181
+ await identifyUser.load(Context.system(), {} as Analytics)
182
+ const fs = jest.spyOn(window.FS, 'setUserVars')
183
+
184
+ await identifyUser.identify?.(
185
+ new Context({
186
+ type: 'identify',
187
+ traits: {
188
+ name: 'Hasbulla',
189
+ email: 'thegoat@world',
190
+ height: '50cm'
191
+ }
192
+ })
193
+ )
194
+
195
+ expect(fs).toHaveBeenCalledWith(
196
+ {
197
+ displayName: 'Hasbulla',
198
+ email: 'thegoat@world',
199
+ height: '50cm',
200
+ name: 'Hasbulla'
201
+ },
202
+ 'segment-browser-actions'
203
+ )
204
+ })
205
+ })
@@ -0,0 +1,16 @@
1
+ // Generated file. DO NOT MODIFY IT BY HAND.
2
+
3
+ export interface Settings {
4
+ /**
5
+ * The organization ID for FullStory.
6
+ */
7
+ orgId: string
8
+ /**
9
+ * Enables FullStory debug mode.
10
+ */
11
+ debug?: boolean
12
+ /**
13
+ * Enables FullStory inside an iframe.
14
+ */
15
+ recordOnlyThisIFrame?: boolean
16
+ }
@@ -0,0 +1,26 @@
1
+ // Generated file. DO NOT MODIFY IT BY HAND.
2
+
3
+ export interface Payload {
4
+ /**
5
+ * The user's id
6
+ */
7
+ userId?: string
8
+ /**
9
+ * The user's anonymous id
10
+ */
11
+ anonymousId?: string
12
+ /**
13
+ * The user's display name
14
+ */
15
+ displayName?: string
16
+ /**
17
+ * The user's email
18
+ */
19
+ email?: string
20
+ /**
21
+ * The Segment traits to be forwarded to FullStory
22
+ */
23
+ traits?: {
24
+ [k: string]: unknown
25
+ }
26
+ }
@@ -0,0 +1,123 @@
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 { FS } from '../types'
5
+ import camelCase from 'lodash/camelCase'
6
+ import { segmentEventSource } from '..'
7
+
8
+ // Change from unknown to the partner SDK types
9
+ const action: BrowserActionDefinition<Settings, FS, Payload> = {
10
+ title: 'Identify User',
11
+ description: 'Sets user identity variables',
12
+ platform: 'web',
13
+ defaultSubscription: 'type = "identify"',
14
+ fields: {
15
+ userId: {
16
+ type: 'string',
17
+ required: false,
18
+ description: "The user's id",
19
+ label: 'User ID',
20
+ default: {
21
+ '@path': '$.userId'
22
+ }
23
+ },
24
+ anonymousId: {
25
+ type: 'string',
26
+ required: false,
27
+ description: "The user's anonymous id",
28
+ label: 'Anonymous ID',
29
+ default: {
30
+ '@path': '$.anonymousId'
31
+ }
32
+ },
33
+ displayName: {
34
+ type: 'string',
35
+ required: false,
36
+ description: "The user's display name",
37
+ label: 'Display Name',
38
+ default: {
39
+ '@path': '$.traits.name'
40
+ }
41
+ },
42
+ email: {
43
+ type: 'string',
44
+ required: false,
45
+ description: "The user's email",
46
+ label: 'Email',
47
+ default: {
48
+ '@path': '$.traits.email'
49
+ }
50
+ },
51
+ traits: {
52
+ type: 'object',
53
+ required: false,
54
+ description: 'The Segment traits to be forwarded to FullStory',
55
+ label: 'Traits',
56
+ default: {
57
+ '@path': '$.traits'
58
+ }
59
+ }
60
+ },
61
+ perform: (FS, event) => {
62
+ let newTraits: Record<string, unknown> = {}
63
+
64
+ if (event.payload.traits) {
65
+ newTraits = Object.entries(event.payload.traits).reduce(
66
+ (acc, [key, value]) => ({
67
+ ...acc,
68
+ [camelCaseField(key)]: value
69
+ }),
70
+ {}
71
+ )
72
+ }
73
+
74
+ if (event.payload.anonymousId) {
75
+ newTraits.segmentAnonymousId_str = event.payload.anonymousId
76
+ }
77
+
78
+ if (event.payload.userId) {
79
+ FS.identify(event.payload.userId, newTraits, segmentEventSource)
80
+ } else {
81
+ FS.setUserVars({
82
+ ...newTraits,
83
+ ...(event.payload.email !== undefined && { email: event.payload.email }),
84
+ ...(event.payload.displayName !== undefined && { displayName: event.payload.displayName })
85
+ }, segmentEventSource)
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Camel cases `.`, `-`, `_`, and white space within fieldNames. Leaves type suffix alone.
92
+ *
93
+ * NOTE: Does not fix otherwise malformed fieldNames.
94
+ * FullStory will scrub characters from keys that do not conform to /^[a-zA-Z][a-zA-Z0-9_]*$/.
95
+ *
96
+ * @param {string} fieldName
97
+ */
98
+ function camelCaseField(fieldName: string) {
99
+ // Do not camel case across type suffixes.
100
+ const parts = fieldName.split('_')
101
+ if (parts.length > 1) {
102
+ const typeSuffix = parts.pop()
103
+ switch (typeSuffix) {
104
+ case 'str':
105
+ case 'int':
106
+ case 'date':
107
+ case 'real':
108
+ case 'bool':
109
+ case 'strs':
110
+ case 'ints':
111
+ case 'dates':
112
+ case 'reals':
113
+ case 'bools':
114
+ return camelCase(parts.join('_')) + '_' + typeSuffix
115
+ default: // passthrough
116
+ }
117
+ }
118
+
119
+ // No type suffix found. Camel case the whole field name.
120
+ return camelCase(fieldName)
121
+ }
122
+
123
+ export default action
package/src/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ import type { FS } from './types'
2
+ import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
3
+ import { FSPackage } from './types'
4
+ import { browserDestination } from '@segment/browser-destination-runtime/shim'
5
+ import type { Settings } from './generated-types'
6
+ import trackEvent from './trackEvent'
7
+ import identifyUser from './identifyUser'
8
+ import viewedPage from './viewedPage'
9
+ import { defaultValues } from '@segment/actions-core'
10
+
11
+ declare global {
12
+ interface Window {
13
+ FS: FS
14
+ }
15
+ }
16
+
17
+ export const segmentEventSource = 'segment-browser-actions'
18
+
19
+ export const destination: BrowserDestinationDefinition<Settings, FS> = {
20
+ name: 'Fullstory (Actions)',
21
+ slug: 'actions-fullstory',
22
+ mode: 'device',
23
+ presets: [
24
+ {
25
+ name: 'Track Event',
26
+ subscribe: 'type = "track"',
27
+ partnerAction: 'trackEvent',
28
+ mapping: defaultValues(trackEvent.fields)
29
+ },
30
+ {
31
+ name: 'Identify User',
32
+ subscribe: 'type = "identify"',
33
+ partnerAction: 'identifyUser',
34
+ mapping: defaultValues(identifyUser.fields)
35
+ }
36
+ ],
37
+ settings: {
38
+ orgId: {
39
+ description: 'The organization ID for FullStory.',
40
+ label: 'FS Org',
41
+ type: 'string',
42
+ required: true
43
+ },
44
+ debug: {
45
+ description: 'Enables FullStory debug mode.',
46
+ label: 'Debug mode',
47
+ type: 'boolean',
48
+ required: false,
49
+ default: false
50
+ },
51
+ recordOnlyThisIFrame: {
52
+ description: 'Enables FullStory inside an iframe.',
53
+ label: 'Capture only this iFrame',
54
+ type: 'boolean',
55
+ required: false,
56
+ default: false
57
+ }
58
+ },
59
+ actions: {
60
+ trackEvent,
61
+ identifyUser,
62
+ viewedPage
63
+ },
64
+ initialize: async ({ settings }, dependencies) => {
65
+ FSPackage.init(settings)
66
+ await dependencies.resolveWhen(() => Object.prototype.hasOwnProperty.call(window, 'FS'), 100)
67
+ return window.FS
68
+ }
69
+ }
70
+
71
+ export default browserDestination(destination)
@@ -0,0 +1,14 @@
1
+ // Generated file. DO NOT MODIFY IT BY HAND.
2
+
3
+ export interface Payload {
4
+ /**
5
+ * The name of the event.
6
+ */
7
+ name: string
8
+ /**
9
+ * A JSON object containing additional information about the event that will be indexed by FullStory.
10
+ */
11
+ properties?: {
12
+ [k: string]: unknown
13
+ }
14
+ }
@@ -0,0 +1,37 @@
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 { FS } from '../types'
5
+ import { segmentEventSource } from '..'
6
+
7
+ const action: BrowserActionDefinition<Settings, FS, Payload> = {
8
+ title: 'Track Event',
9
+ description: 'Track events',
10
+ platform: 'web',
11
+ defaultSubscription: 'type = "track"',
12
+ fields: {
13
+ name: {
14
+ description: 'The name of the event.',
15
+ label: 'Name',
16
+ required: true,
17
+ type: 'string',
18
+ default: {
19
+ '@path': '$.event'
20
+ }
21
+ },
22
+ properties: {
23
+ description: 'A JSON object containing additional information about the event that will be indexed by FullStory.',
24
+ label: 'Properties',
25
+ required: false,
26
+ type: 'object',
27
+ default: {
28
+ '@path': '$.properties'
29
+ }
30
+ }
31
+ },
32
+ perform: (FS, event) => {
33
+ FS.event(event.payload.name, event.payload.properties ?? {}, segmentEventSource)
34
+ }
35
+ }
36
+
37
+ export default action
package/src/types.ts ADDED
@@ -0,0 +1,10 @@
1
+ import * as FullStory from '@fullstory/browser'
2
+
3
+ export const FSPackage = FullStory
4
+ export type FS = typeof FullStory & {
5
+ // setVars is not available on the FS client yet.
6
+ setVars: (eventName: string, eventProperties: object, source: string) => {}
7
+ setUserVars: (eventProperties: object, source: string) => void
8
+ event: (eventName: string, eventProperties: { [key: string]: unknown }, source: string) => void
9
+ identify: (uid: string, customVars: FullStory.UserVars, source: string) => void
10
+ }
@@ -0,0 +1,14 @@
1
+ // Generated file. DO NOT MODIFY IT BY HAND.
2
+
3
+ export interface Payload {
4
+ /**
5
+ * The name of the page that was viewed.
6
+ */
7
+ pageName?: string
8
+ /**
9
+ * The properties of the page that was viewed.
10
+ */
11
+ properties?: {
12
+ [k: string]: unknown
13
+ }
14
+ }
@@ -0,0 +1,45 @@
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 { FS } from '../types'
5
+ import { segmentEventSource } from '..'
6
+
7
+ const action: BrowserActionDefinition<Settings, FS, Payload> = {
8
+ title: 'Viewed Page',
9
+ description: 'Sets page properties events',
10
+ defaultSubscription: 'type = "page"',
11
+ platform: 'web',
12
+ fields: {
13
+ pageName: {
14
+ type: 'string',
15
+ required: false,
16
+ description: 'The name of the page that was viewed.',
17
+ label: 'Page Name',
18
+ default: {
19
+ '@if': {
20
+ exists: { '@path': '$.category' },
21
+ then: { '@path': '$.category' },
22
+ else: { '@path': '$.name' }
23
+ }
24
+ }
25
+ },
26
+ properties: {
27
+ type: 'object',
28
+ required: false,
29
+ description: 'The properties of the page that was viewed.',
30
+ label: 'Properties',
31
+ default: {
32
+ '@path': '$.properties'
33
+ }
34
+ }
35
+ },
36
+ perform: (FS, event) => {
37
+ if (event.payload.pageName) {
38
+ FS.setVars('page', { pageName: event.payload.pageName, ...event.payload.properties }, segmentEventSource)
39
+ } else if (event.payload.properties) {
40
+ FS.setVars('page', event.payload.properties, segmentEventSource)
41
+ }
42
+ }
43
+ }
44
+
45
+ export default action
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.build.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "baseUrl": "."
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["dist", "**/__tests__"]
9
+ }