@segment/analytics-browser-actions-logrocket 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 +21 -0
- package/src/__tests__/index.test.ts +105 -0
- package/src/generated-types.ts +16 -0
- package/src/identify/__tests__/index.test.ts +50 -0
- package/src/identify/generated-types.ts +14 -0
- package/src/identify/index.ts +45 -0
- package/src/index.ts +93 -0
- package/src/test_utilities.ts +55 -0
- package/src/track/__tests__/index.test.ts +30 -0
- package/src/track/generated-types.ts +14 -0
- package/src/track/index.ts +40 -0
- package/src/types.ts +3 -0
- package/tsconfig.json +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@segment/analytics-browser-actions-logrocket",
|
|
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
|
+
"@segment/actions-core": "^3.71.0",
|
|
15
|
+
"@segment/browser-destination-runtime": "^1.0.0",
|
|
16
|
+
"logrocket": "^3.0.1"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@segment/analytics-next": "*"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Analytics, Context } from '@segment/analytics-next'
|
|
2
|
+
import plugins, { destination } from '../index'
|
|
3
|
+
import { mockWorkerAndXMLHttpRequest, subscriptions } from '../test_utilities'
|
|
4
|
+
import Logrocket from 'logrocket'
|
|
5
|
+
|
|
6
|
+
jest.mock('logrocket')
|
|
7
|
+
|
|
8
|
+
const appID = 'log/rocket'
|
|
9
|
+
|
|
10
|
+
describe('Logrocket', () => {
|
|
11
|
+
beforeAll(mockWorkerAndXMLHttpRequest)
|
|
12
|
+
afterAll(jest.restoreAllMocks)
|
|
13
|
+
|
|
14
|
+
test('can load', async () => {
|
|
15
|
+
const [event] = await plugins({
|
|
16
|
+
appID,
|
|
17
|
+
networkSanitization: false,
|
|
18
|
+
inputSanitization: false,
|
|
19
|
+
subscriptions
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
jest.spyOn(destination, 'initialize')
|
|
23
|
+
jest.spyOn(Logrocket, 'init')
|
|
24
|
+
|
|
25
|
+
await event.load(Context.system(), {} as Analytics)
|
|
26
|
+
expect(destination.initialize).toHaveBeenCalled()
|
|
27
|
+
expect(Logrocket.init).toHaveBeenCalled()
|
|
28
|
+
|
|
29
|
+
expect(window._LRLogger).toBeDefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('supplies the input sanitization parameter', async () => {
|
|
33
|
+
const [event] = await plugins({
|
|
34
|
+
appID,
|
|
35
|
+
networkSanitization: false,
|
|
36
|
+
inputSanitization: true,
|
|
37
|
+
subscriptions
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
jest.spyOn(Logrocket, 'init')
|
|
41
|
+
|
|
42
|
+
await event.load(Context.system(), {} as Analytics)
|
|
43
|
+
expect(Logrocket.init).toHaveBeenCalledWith(appID, expect.objectContaining({ dom: { inputSanitizer: true } }))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('network sanitizer', () => {
|
|
47
|
+
test('redacts requests when configured', async () => {
|
|
48
|
+
const [event] = await plugins({
|
|
49
|
+
appID,
|
|
50
|
+
networkSanitization: true,
|
|
51
|
+
inputSanitization: false,
|
|
52
|
+
subscriptions
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const spy = jest.spyOn(Logrocket, 'init')
|
|
56
|
+
|
|
57
|
+
await event.load(Context.system(), {} as Analytics)
|
|
58
|
+
expect(Logrocket.init).toHaveBeenCalledWith(appID, expect.objectContaining({ dom: { inputSanitizer: false } }))
|
|
59
|
+
const requestSanitizer: RequestSanitizer = spy.mock.calls[0][1]?.network?.requestSanitizer
|
|
60
|
+
|
|
61
|
+
if (!requestSanitizer) fail('request sanitizer null')
|
|
62
|
+
|
|
63
|
+
const mockRequest = {
|
|
64
|
+
body: 'hello',
|
|
65
|
+
headers: { goodbye: 'moon' },
|
|
66
|
+
reqId: 'something',
|
|
67
|
+
url: 'neat',
|
|
68
|
+
method: 'get'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sanitizedResult = requestSanitizer(mockRequest)
|
|
72
|
+
|
|
73
|
+
expect(sanitizedResult).toEqual(expect.objectContaining({ body: undefined, headers: {} }))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('does not modify requests if disabled', async () => {
|
|
77
|
+
const [event] = await plugins({
|
|
78
|
+
appID,
|
|
79
|
+
networkSanitization: false,
|
|
80
|
+
inputSanitization: false,
|
|
81
|
+
subscriptions
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const spy = jest.spyOn(Logrocket, 'init')
|
|
85
|
+
|
|
86
|
+
await event.load(Context.system(), {} as Analytics)
|
|
87
|
+
expect(Logrocket.init).toHaveBeenCalledWith(appID, expect.objectContaining({ dom: { inputSanitizer: false } }))
|
|
88
|
+
const requestSanitizer: RequestSanitizer = spy.mock.calls[0][1]?.network?.requestSanitizer
|
|
89
|
+
|
|
90
|
+
if (!requestSanitizer) fail('request sanitizer null')
|
|
91
|
+
|
|
92
|
+
const mockRequest = {
|
|
93
|
+
body: 'hello',
|
|
94
|
+
headers: { goodbye: 'moon' },
|
|
95
|
+
reqId: 'something',
|
|
96
|
+
url: 'neat',
|
|
97
|
+
method: 'get'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const sanitizedResult = requestSanitizer(mockRequest)
|
|
101
|
+
|
|
102
|
+
expect(sanitizedResult).toEqual(mockRequest)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Generated file. DO NOT MODIFY IT BY HAND.
|
|
2
|
+
|
|
3
|
+
export interface Settings {
|
|
4
|
+
/**
|
|
5
|
+
* The LogRocket app ID.
|
|
6
|
+
*/
|
|
7
|
+
appID: string
|
|
8
|
+
/**
|
|
9
|
+
* Sanitize all network request and response bodies from session recordings.
|
|
10
|
+
*/
|
|
11
|
+
networkSanitization: boolean
|
|
12
|
+
/**
|
|
13
|
+
* Obfuscate all user-input elements (like <input> and <select>) from session recordings.
|
|
14
|
+
*/
|
|
15
|
+
inputSanitization: boolean
|
|
16
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Analytics, Context } from '@segment/analytics-next'
|
|
2
|
+
import plugins from '../../index'
|
|
3
|
+
import LogRocket from 'logrocket'
|
|
4
|
+
import { identifySubscription, mockWorkerAndXMLHttpRequest } from '../../test_utilities'
|
|
5
|
+
|
|
6
|
+
describe('Logrocket.identify', () => {
|
|
7
|
+
const settings = { appID: 'log/rocket' }
|
|
8
|
+
|
|
9
|
+
beforeAll(mockWorkerAndXMLHttpRequest)
|
|
10
|
+
afterAll(jest.restoreAllMocks)
|
|
11
|
+
|
|
12
|
+
const traits = {
|
|
13
|
+
goodbye: 'moon'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
it('should send user ID and traits to logrocket', async () => {
|
|
17
|
+
const [identify] = await plugins({ ...settings, subscriptions: [identifySubscription] })
|
|
18
|
+
|
|
19
|
+
await identify.load(Context.system(), {} as Analytics)
|
|
20
|
+
const identifySpy = jest.spyOn(LogRocket, 'identify')
|
|
21
|
+
|
|
22
|
+
const userId = 'user1'
|
|
23
|
+
|
|
24
|
+
await identify.identify?.(
|
|
25
|
+
new Context({
|
|
26
|
+
type: 'identify',
|
|
27
|
+
traits,
|
|
28
|
+
userId
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
expect(identifySpy).toHaveBeenCalledWith(userId, traits)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("shouldn't send an ID if the user is anonymous", async () => {
|
|
36
|
+
const [identify] = await plugins({ appID: 'log/rocket', subscriptions: [identifySubscription] })
|
|
37
|
+
|
|
38
|
+
await identify.load(Context.system(), {} as Analytics)
|
|
39
|
+
const identifySpy = jest.spyOn(LogRocket, 'identify')
|
|
40
|
+
|
|
41
|
+
await identify.identify?.(
|
|
42
|
+
new Context({
|
|
43
|
+
type: 'identify',
|
|
44
|
+
traits
|
|
45
|
+
})
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
expect(identifySpy).toHaveBeenCalledWith(traits)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Generated file. DO NOT MODIFY IT BY HAND.
|
|
2
|
+
|
|
3
|
+
export interface Payload {
|
|
4
|
+
/**
|
|
5
|
+
* user id
|
|
6
|
+
*/
|
|
7
|
+
userId?: string
|
|
8
|
+
/**
|
|
9
|
+
* A JSON object containing additional traits that will be associated with the user.
|
|
10
|
+
*/
|
|
11
|
+
traits?: {
|
|
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 { LR } from '../types'
|
|
5
|
+
|
|
6
|
+
type Traits = {
|
|
7
|
+
[propName: string]: string | number | boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const action: BrowserActionDefinition<Settings, LR, Payload> = {
|
|
11
|
+
title: 'Identify',
|
|
12
|
+
description: 'Send identification information to logrocket.',
|
|
13
|
+
platform: 'web',
|
|
14
|
+
defaultSubscription: 'type = "identify"',
|
|
15
|
+
fields: {
|
|
16
|
+
userId: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
required: false,
|
|
19
|
+
description: 'user id',
|
|
20
|
+
label: 'User ID',
|
|
21
|
+
default: {
|
|
22
|
+
'@path': '$.userId'
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
traits: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
required: false,
|
|
28
|
+
description: 'A JSON object containing additional traits that will be associated with the user.',
|
|
29
|
+
label: 'Traits',
|
|
30
|
+
default: {
|
|
31
|
+
'@path': '$.traits'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
perform: (LogRocket, event) => {
|
|
36
|
+
const { userId, traits } = event.payload
|
|
37
|
+
if (userId) {
|
|
38
|
+
LogRocket.identify(userId, traits as Traits)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
LogRocket.identify(traits as Traits)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default action
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { LR } from './types'
|
|
2
|
+
import type { Settings } from './generated-types'
|
|
3
|
+
import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types'
|
|
4
|
+
import { browserDestination } from '@segment/browser-destination-runtime/shim'
|
|
5
|
+
import LogRocket from 'logrocket'
|
|
6
|
+
import track from './track'
|
|
7
|
+
import identify from './identify'
|
|
8
|
+
import { defaultValues } from '@segment/actions-core'
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
interface Window {
|
|
12
|
+
LogRocket: LR
|
|
13
|
+
logRocketSettings?: LogRocketSettings
|
|
14
|
+
_LRLogger: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type LogRocketSettings = NonNullable<Parameters<LR['init']>[1]>
|
|
18
|
+
type RequestSanitizer = NonNullable<LogRocketSettings['network']>['requestSanitizer']
|
|
19
|
+
}
|
|
20
|
+
// Switch from unknown to the partner SDK client types
|
|
21
|
+
export const destination: BrowserDestinationDefinition<Settings, LR> = {
|
|
22
|
+
name: 'Logrocket',
|
|
23
|
+
slug: 'actions-logrocket',
|
|
24
|
+
mode: 'device',
|
|
25
|
+
|
|
26
|
+
presets: [
|
|
27
|
+
{
|
|
28
|
+
name: 'Track',
|
|
29
|
+
subscribe: 'type = "track"',
|
|
30
|
+
partnerAction: 'track',
|
|
31
|
+
mapping: defaultValues(track.fields)
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'Identify',
|
|
35
|
+
subscribe: 'type = "identify"',
|
|
36
|
+
partnerAction: 'identify',
|
|
37
|
+
mapping: defaultValues(identify.fields)
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
|
|
41
|
+
settings: {
|
|
42
|
+
appID: {
|
|
43
|
+
description: 'The LogRocket app ID.',
|
|
44
|
+
label: 'LogRocket App',
|
|
45
|
+
type: 'string',
|
|
46
|
+
required: true
|
|
47
|
+
},
|
|
48
|
+
networkSanitization: {
|
|
49
|
+
description: 'Sanitize all network request and response bodies from session recordings.',
|
|
50
|
+
label: 'Network Sanitization',
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
required: true,
|
|
53
|
+
default: true
|
|
54
|
+
},
|
|
55
|
+
inputSanitization: {
|
|
56
|
+
description: 'Obfuscate all user-input elements (like <input> and <select>) from session recordings.',
|
|
57
|
+
label: 'Input Sanitization',
|
|
58
|
+
type: 'boolean',
|
|
59
|
+
required: true,
|
|
60
|
+
default: true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
initialize: async ({ settings: { appID, inputSanitization: inputSanitizer, networkSanitization } }, deps) => {
|
|
65
|
+
const requestSanitizer: RequestSanitizer = (request) => {
|
|
66
|
+
if (networkSanitization) {
|
|
67
|
+
request.body = undefined
|
|
68
|
+
request.headers = {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return request
|
|
72
|
+
}
|
|
73
|
+
const settings: LogRocketSettings = {
|
|
74
|
+
dom: {
|
|
75
|
+
inputSanitizer
|
|
76
|
+
},
|
|
77
|
+
network: {
|
|
78
|
+
requestSanitizer,
|
|
79
|
+
responseSanitizer: requestSanitizer
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
LogRocket.init(appID, window.logRocketSettings || settings)
|
|
83
|
+
await deps.resolveWhen(() => Object.prototype.hasOwnProperty.call(window, '_LRLogger'), 100)
|
|
84
|
+
return LogRocket
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
actions: {
|
|
88
|
+
track,
|
|
89
|
+
identify
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default browserDestination(destination)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Subscription } from '@segment/browser-destination-runtime/types'
|
|
2
|
+
|
|
3
|
+
class WorkerStub {
|
|
4
|
+
url: string
|
|
5
|
+
onmessage: (_arg: string) => void
|
|
6
|
+
constructor(stringUrl: string) {
|
|
7
|
+
this.url = stringUrl
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
9
|
+
this.onmessage = (_arg: string) => {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
postMessage(msg: string) {
|
|
13
|
+
this.onmessage(msg)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
17
|
+
addEventListener() {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mockWorkerAndXMLHttpRequest(): void {
|
|
21
|
+
window.XMLHttpRequest = jest.fn() as any
|
|
22
|
+
window.Worker = WorkerStub as any
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const trackSubscription: Subscription = {
|
|
26
|
+
partnerAction: 'track',
|
|
27
|
+
name: 'Track',
|
|
28
|
+
enabled: true,
|
|
29
|
+
subscribe: 'type = "track"',
|
|
30
|
+
mapping: {
|
|
31
|
+
name: {
|
|
32
|
+
'@path': '$.name'
|
|
33
|
+
},
|
|
34
|
+
properties: {
|
|
35
|
+
'@path': '$.properties'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const identifySubscription: Subscription = {
|
|
41
|
+
partnerAction: 'identify',
|
|
42
|
+
name: 'Identify',
|
|
43
|
+
enabled: true,
|
|
44
|
+
subscribe: 'type = "identify"',
|
|
45
|
+
mapping: {
|
|
46
|
+
userId: {
|
|
47
|
+
'@path': '$.userId'
|
|
48
|
+
},
|
|
49
|
+
traits: {
|
|
50
|
+
'@path': '$.traits'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const subscriptions = [trackSubscription, identifySubscription]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Analytics, Context } from '@segment/analytics-next'
|
|
2
|
+
import plugins from '../../index'
|
|
3
|
+
import LogRocket from 'logrocket'
|
|
4
|
+
import { mockWorkerAndXMLHttpRequest, trackSubscription } from '../../test_utilities'
|
|
5
|
+
|
|
6
|
+
describe('Logrocket.track', () => {
|
|
7
|
+
beforeAll(mockWorkerAndXMLHttpRequest)
|
|
8
|
+
afterAll(jest.restoreAllMocks)
|
|
9
|
+
it('sends track events to logrocket', async () => {
|
|
10
|
+
const [event] = await plugins({ appID: 'log/rocket', subscriptions: [trackSubscription] })
|
|
11
|
+
|
|
12
|
+
await event.load(Context.system(), {} as Analytics)
|
|
13
|
+
const trackSpy = jest.spyOn(LogRocket, 'track')
|
|
14
|
+
|
|
15
|
+
const name = 'testName'
|
|
16
|
+
const properties = {
|
|
17
|
+
goodbye: 'moon'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await event.track?.(
|
|
21
|
+
new Context({
|
|
22
|
+
type: 'track',
|
|
23
|
+
name,
|
|
24
|
+
properties
|
|
25
|
+
})
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
expect(trackSpy).toHaveBeenCalledWith(name, properties)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -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 properties that will be associated with the event.
|
|
10
|
+
*/
|
|
11
|
+
properties?: {
|
|
12
|
+
[k: string]: unknown
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { LR } from '../types'
|
|
5
|
+
|
|
6
|
+
type TrackEventProperties = {
|
|
7
|
+
[key: string]: string | number | boolean | string[] | number[] | boolean[] | undefined
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const action: BrowserActionDefinition<Settings, LR, Payload> = {
|
|
11
|
+
title: 'Track',
|
|
12
|
+
description: 'Send track events to logrocket for filtering and tagging.',
|
|
13
|
+
platform: 'web',
|
|
14
|
+
defaultSubscription: 'type = "track"',
|
|
15
|
+
fields: {
|
|
16
|
+
name: {
|
|
17
|
+
description: 'The name of the event.',
|
|
18
|
+
label: 'Name',
|
|
19
|
+
required: true,
|
|
20
|
+
type: 'string',
|
|
21
|
+
default: {
|
|
22
|
+
'@path': '$.event'
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
properties: {
|
|
26
|
+
description: 'A JSON object containing additional properties that will be associated with the event.',
|
|
27
|
+
label: 'Properties',
|
|
28
|
+
required: false,
|
|
29
|
+
type: 'object',
|
|
30
|
+
default: {
|
|
31
|
+
'@path': '$.properties'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
perform: (LogRocket, event) => {
|
|
36
|
+
LogRocket.track(event.payload.name, (event.payload.properties as TrackEventProperties) ?? {})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default action
|
package/src/types.ts
ADDED