@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 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
@@ -0,0 +1,3 @@
1
+ import * as LogRocket from 'logrocket'
2
+
3
+ export type LR = typeof LogRocket
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
+ }