@customerio/cdp-analytics-node 0.0.1
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/LICENSE.MD +22 -0
- package/README.md +20 -0
- package/dist/cjs/app/analytics-node.js +170 -0
- package/dist/cjs/app/analytics-node.js.map +1 -0
- package/dist/cjs/app/context.js +13 -0
- package/dist/cjs/app/context.js.map +1 -0
- package/dist/cjs/app/dispatch-emit.js +37 -0
- package/dist/cjs/app/dispatch-emit.js.map +1 -0
- package/dist/cjs/app/emitter.js +8 -0
- package/dist/cjs/app/emitter.js.map +1 -0
- package/dist/cjs/app/event-factory.js +12 -0
- package/dist/cjs/app/event-factory.js.map +1 -0
- package/dist/cjs/app/event-queue.js +24 -0
- package/dist/cjs/app/event-queue.js.map +1 -0
- package/dist/cjs/app/settings.js +11 -0
- package/dist/cjs/app/settings.js.map +1 -0
- package/dist/cjs/app/types/event.js +3 -0
- package/dist/cjs/app/types/event.js.map +1 -0
- package/dist/cjs/app/types/index.js +7 -0
- package/dist/cjs/app/types/index.js.map +1 -0
- package/dist/cjs/app/types/params.js +3 -0
- package/dist/cjs/app/types/params.js.map +1 -0
- package/dist/cjs/app/types/plugin.js +3 -0
- package/dist/cjs/app/types/plugin.js.map +1 -0
- package/dist/cjs/generated/version.js +6 -0
- package/dist/cjs/generated/version.js.map +1 -0
- package/dist/cjs/index.js +11 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/lib/abort.js +77 -0
- package/dist/cjs/lib/abort.js.map +1 -0
- package/dist/cjs/lib/base-64-encode.js +19 -0
- package/dist/cjs/lib/base-64-encode.js.map +1 -0
- package/dist/cjs/lib/create-url.js +15 -0
- package/dist/cjs/lib/create-url.js.map +1 -0
- package/dist/cjs/lib/env.js +25 -0
- package/dist/cjs/lib/env.js.map +1 -0
- package/dist/cjs/lib/extract-promise-parts.js +21 -0
- package/dist/cjs/lib/extract-promise-parts.js.map +1 -0
- package/dist/cjs/lib/fetch.js +7 -0
- package/dist/cjs/lib/fetch.js.map +1 -0
- package/dist/cjs/lib/get-message-id.js +14 -0
- package/dist/cjs/lib/get-message-id.js.map +1 -0
- package/dist/cjs/lib/uuid.js +6 -0
- package/dist/cjs/lib/uuid.js.map +1 -0
- package/dist/cjs/plugins/customerio/context-batch.js +55 -0
- package/dist/cjs/plugins/customerio/context-batch.js.map +1 -0
- package/dist/cjs/plugins/customerio/index.js +44 -0
- package/dist/cjs/plugins/customerio/index.js.map +1 -0
- package/dist/cjs/plugins/customerio/publisher.js +189 -0
- package/dist/cjs/plugins/customerio/publisher.js.map +1 -0
- package/dist/esm/app/analytics-node.js +166 -0
- package/dist/esm/app/analytics-node.js.map +1 -0
- package/dist/esm/app/context.js +9 -0
- package/dist/esm/app/context.js.map +1 -0
- package/dist/esm/app/dispatch-emit.js +33 -0
- package/dist/esm/app/dispatch-emit.js.map +1 -0
- package/dist/esm/app/emitter.js +4 -0
- package/dist/esm/app/emitter.js.map +1 -0
- package/dist/esm/app/event-factory.js +8 -0
- package/dist/esm/app/event-factory.js.map +1 -0
- package/dist/esm/app/event-queue.js +20 -0
- package/dist/esm/app/event-queue.js.map +1 -0
- package/dist/esm/app/settings.js +7 -0
- package/dist/esm/app/settings.js.map +1 -0
- package/dist/esm/app/types/event.js +2 -0
- package/dist/esm/app/types/event.js.map +1 -0
- package/dist/esm/app/types/index.js +4 -0
- package/dist/esm/app/types/index.js.map +1 -0
- package/dist/esm/app/types/params.js +2 -0
- package/dist/esm/app/types/params.js.map +1 -0
- package/dist/esm/app/types/plugin.js +2 -0
- package/dist/esm/app/types/plugin.js.map +1 -0
- package/dist/esm/generated/version.js +3 -0
- package/dist/esm/generated/version.js.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/lib/abort.js +73 -0
- package/dist/esm/lib/abort.js.map +1 -0
- package/dist/esm/lib/base-64-encode.js +15 -0
- package/dist/esm/lib/base-64-encode.js.map +1 -0
- package/dist/esm/lib/create-url.js +11 -0
- package/dist/esm/lib/create-url.js.map +1 -0
- package/dist/esm/lib/env.js +21 -0
- package/dist/esm/lib/env.js.map +1 -0
- package/dist/esm/lib/extract-promise-parts.js +17 -0
- package/dist/esm/lib/extract-promise-parts.js.map +1 -0
- package/dist/esm/lib/fetch.js +3 -0
- package/dist/esm/lib/fetch.js.map +1 -0
- package/dist/esm/lib/get-message-id.js +10 -0
- package/dist/esm/lib/get-message-id.js.map +1 -0
- package/dist/esm/lib/uuid.js +2 -0
- package/dist/esm/lib/uuid.js.map +1 -0
- package/dist/esm/plugins/customerio/context-batch.js +51 -0
- package/dist/esm/plugins/customerio/context-batch.js.map +1 -0
- package/dist/esm/plugins/customerio/index.js +39 -0
- package/dist/esm/plugins/customerio/index.js.map +1 -0
- package/dist/esm/plugins/customerio/publisher.js +185 -0
- package/dist/esm/plugins/customerio/publisher.js.map +1 -0
- package/dist/types/app/analytics-node.d.ts +62 -0
- package/dist/types/app/analytics-node.d.ts.map +1 -0
- package/dist/types/app/context.d.ts +6 -0
- package/dist/types/app/context.d.ts.map +1 -0
- package/dist/types/app/dispatch-emit.d.ts +7 -0
- package/dist/types/app/dispatch-emit.d.ts.map +1 -0
- package/dist/types/app/emitter.d.ts +23 -0
- package/dist/types/app/emitter.d.ts.map +1 -0
- package/dist/types/app/event-factory.d.ts +14 -0
- package/dist/types/app/event-factory.d.ts.map +1 -0
- package/dist/types/app/event-queue.d.ts +7 -0
- package/dist/types/app/event-queue.d.ts.map +1 -0
- package/dist/types/app/settings.d.ts +33 -0
- package/dist/types/app/settings.d.ts.map +1 -0
- package/dist/types/app/types/event.d.ts +7 -0
- package/dist/types/app/types/event.d.ts.map +1 -0
- package/dist/types/app/types/index.d.ts +4 -0
- package/dist/types/app/types/index.d.ts.map +1 -0
- package/dist/types/app/types/params.d.ts +60 -0
- package/dist/types/app/types/params.d.ts.map +1 -0
- package/dist/types/app/types/plugin.d.ts +6 -0
- package/dist/types/app/types/plugin.d.ts.map +1 -0
- package/dist/types/generated/version.d.ts +2 -0
- package/dist/types/generated/version.d.ts.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/lib/abort.d.ts +6 -0
- package/dist/types/lib/abort.d.ts.map +1 -0
- package/dist/types/lib/base-64-encode.d.ts +5 -0
- package/dist/types/lib/base-64-encode.d.ts.map +1 -0
- package/dist/types/lib/create-url.d.ts +8 -0
- package/dist/types/lib/create-url.d.ts.map +1 -0
- package/dist/types/lib/env.d.ts +3 -0
- package/dist/types/lib/env.d.ts.map +1 -0
- package/dist/types/lib/extract-promise-parts.d.ts +9 -0
- package/dist/types/lib/extract-promise-parts.d.ts.map +1 -0
- package/dist/types/lib/fetch.d.ts +2 -0
- package/dist/types/lib/fetch.d.ts.map +1 -0
- package/dist/types/lib/get-message-id.d.ts +7 -0
- package/dist/types/lib/get-message-id.d.ts.map +1 -0
- package/dist/types/lib/uuid.d.ts +2 -0
- package/dist/types/lib/uuid.d.ts.map +1 -0
- package/dist/types/plugins/customerio/context-batch.d.ts +26 -0
- package/dist/types/plugins/customerio/context-batch.d.ts.map +1 -0
- package/dist/types/plugins/customerio/index.d.ts +13 -0
- package/dist/types/plugins/customerio/index.d.ts.map +1 -0
- package/dist/types/plugins/customerio/publisher.d.ts +38 -0
- package/dist/types/plugins/customerio/publisher.d.ts.map +1 -0
- package/package.json +43 -0
- package/src/app/analytics-node.ts +295 -0
- package/src/app/context.ts +11 -0
- package/src/app/dispatch-emit.ts +42 -0
- package/src/app/emitter.ts +23 -0
- package/src/app/event-factory.ts +20 -0
- package/src/app/event-queue.ts +23 -0
- package/src/app/settings.ts +39 -0
- package/src/app/types/event.ts +7 -0
- package/src/app/types/index.ts +3 -0
- package/src/app/types/params.ts +74 -0
- package/src/app/types/plugin.ts +5 -0
- package/src/generated/version.ts +2 -0
- package/src/index.ts +17 -0
- package/src/lib/abort.ts +77 -0
- package/src/lib/base-64-encode.ts +14 -0
- package/src/lib/create-url.ts +11 -0
- package/src/lib/env.ts +32 -0
- package/src/lib/extract-promise-parts.ts +21 -0
- package/src/lib/fetch.ts +3 -0
- package/src/lib/get-message-id.ts +10 -0
- package/src/lib/uuid.ts +1 -0
- package/src/plugins/customerio/context-batch.ts +71 -0
- package/src/plugins/customerio/index.ts +65 -0
- package/src/plugins/customerio/publisher.ts +258 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CoreAnalytics,
|
|
3
|
+
bindAll,
|
|
4
|
+
pTimeout,
|
|
5
|
+
} from '@customerio/cdp-analytics-core'
|
|
6
|
+
import { AnalyticsSettings, validateSettings } from './settings'
|
|
7
|
+
import { version } from '../generated/version'
|
|
8
|
+
import { createConfiguredNodePlugin } from '../plugins/customerio'
|
|
9
|
+
import { NodeEventFactory } from './event-factory'
|
|
10
|
+
import { Callback, dispatchAndEmit } from './dispatch-emit'
|
|
11
|
+
import { NodeEmitter } from './emitter'
|
|
12
|
+
import {
|
|
13
|
+
AliasParams,
|
|
14
|
+
GroupParams,
|
|
15
|
+
IdentifyParams,
|
|
16
|
+
PageParams,
|
|
17
|
+
TrackParams,
|
|
18
|
+
Plugin,
|
|
19
|
+
CustomerioEvent,
|
|
20
|
+
} from './types'
|
|
21
|
+
import { Context } from './context'
|
|
22
|
+
import { NodeEventQueue } from './event-queue'
|
|
23
|
+
|
|
24
|
+
export class Analytics extends NodeEmitter implements CoreAnalytics {
|
|
25
|
+
private readonly _eventFactory: NodeEventFactory
|
|
26
|
+
private _isClosed = false
|
|
27
|
+
private _pendingEvents = 0
|
|
28
|
+
private readonly _closeAndFlushDefaultTimeout: number
|
|
29
|
+
private readonly _publisher: ReturnType<
|
|
30
|
+
typeof createConfiguredNodePlugin
|
|
31
|
+
>['publisher']
|
|
32
|
+
|
|
33
|
+
private readonly _queue: NodeEventQueue
|
|
34
|
+
|
|
35
|
+
ready: Promise<void>
|
|
36
|
+
|
|
37
|
+
constructor(settings: AnalyticsSettings) {
|
|
38
|
+
super()
|
|
39
|
+
validateSettings(settings)
|
|
40
|
+
|
|
41
|
+
this._eventFactory = new NodeEventFactory()
|
|
42
|
+
this._queue = new NodeEventQueue()
|
|
43
|
+
|
|
44
|
+
const flushInterval = settings.flushInterval ?? 10000
|
|
45
|
+
|
|
46
|
+
this._closeAndFlushDefaultTimeout = flushInterval * 1.25 // add arbitrary multiplier in case an event is in a plugin.
|
|
47
|
+
|
|
48
|
+
const { plugin, publisher } = createConfiguredNodePlugin(
|
|
49
|
+
{
|
|
50
|
+
writeKey: settings.writeKey,
|
|
51
|
+
host: settings.host,
|
|
52
|
+
path: settings.path,
|
|
53
|
+
maxRetries: settings.maxRetries ?? 3,
|
|
54
|
+
maxEventsInBatch: settings.maxEventsInBatch ?? 15,
|
|
55
|
+
httpRequestTimeout: settings.httpRequestTimeout,
|
|
56
|
+
flushInterval,
|
|
57
|
+
},
|
|
58
|
+
this as NodeEmitter
|
|
59
|
+
)
|
|
60
|
+
this._publisher = publisher
|
|
61
|
+
this.ready = this.register(plugin).then(() => undefined)
|
|
62
|
+
|
|
63
|
+
this.emit('initialize', settings)
|
|
64
|
+
|
|
65
|
+
bindAll(this)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get VERSION() {
|
|
69
|
+
return version
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Call this method to stop collecting new events and flush all existing events.
|
|
74
|
+
* This method also waits for any event method-specific callbacks to be triggered,
|
|
75
|
+
* and any of their subsequent promises to be resolved/rejected.
|
|
76
|
+
*/
|
|
77
|
+
public closeAndFlush({
|
|
78
|
+
timeout = this._closeAndFlushDefaultTimeout,
|
|
79
|
+
}: {
|
|
80
|
+
/** Set a maximum time permitted to wait before resolving. */
|
|
81
|
+
timeout?: number
|
|
82
|
+
} = {}): Promise<void> {
|
|
83
|
+
this._publisher.flushAfterClose(this._pendingEvents)
|
|
84
|
+
this._isClosed = true
|
|
85
|
+
const promise = new Promise<void>((resolve) => {
|
|
86
|
+
if (!this._pendingEvents) {
|
|
87
|
+
resolve()
|
|
88
|
+
} else {
|
|
89
|
+
this.once('drained', () => resolve())
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
return timeout ? pTimeout(promise, timeout).catch(() => undefined) : promise
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private _dispatch(CustomerioEvent: CustomerioEvent, callback?: Callback) {
|
|
96
|
+
if (this._isClosed) {
|
|
97
|
+
this.emit('call_after_close', CustomerioEvent as CustomerioEvent)
|
|
98
|
+
return undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this._pendingEvents++
|
|
102
|
+
|
|
103
|
+
dispatchAndEmit(CustomerioEvent, this._queue, this, callback)
|
|
104
|
+
.catch((ctx) => ctx)
|
|
105
|
+
.finally(() => {
|
|
106
|
+
this._pendingEvents--
|
|
107
|
+
|
|
108
|
+
if (!this._pendingEvents) {
|
|
109
|
+
this.emit('drained')
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Combines two unassociated user identities.
|
|
116
|
+
*/
|
|
117
|
+
alias(
|
|
118
|
+
{ userId, previousId, context, timestamp, integrations }: AliasParams,
|
|
119
|
+
callback?: Callback
|
|
120
|
+
): void {
|
|
121
|
+
const CustomerioEvent = this._eventFactory.alias(userId, previousId, {
|
|
122
|
+
context,
|
|
123
|
+
integrations,
|
|
124
|
+
timestamp,
|
|
125
|
+
})
|
|
126
|
+
this._dispatch(CustomerioEvent, callback)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Associates an identified user with a collective.
|
|
131
|
+
*/
|
|
132
|
+
group(
|
|
133
|
+
{
|
|
134
|
+
timestamp,
|
|
135
|
+
groupId,
|
|
136
|
+
userId,
|
|
137
|
+
anonymousId,
|
|
138
|
+
traits = {},
|
|
139
|
+
context,
|
|
140
|
+
integrations,
|
|
141
|
+
}: GroupParams,
|
|
142
|
+
callback?: Callback
|
|
143
|
+
): void {
|
|
144
|
+
const CustomerioEvent = this._eventFactory.group(groupId, traits, {
|
|
145
|
+
context,
|
|
146
|
+
anonymousId,
|
|
147
|
+
userId,
|
|
148
|
+
timestamp,
|
|
149
|
+
integrations,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
this._dispatch(CustomerioEvent, callback)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Includes a unique userId and (maybe anonymousId) and any optional traits you know about them.
|
|
157
|
+
*/
|
|
158
|
+
identify(
|
|
159
|
+
{
|
|
160
|
+
userId,
|
|
161
|
+
anonymousId,
|
|
162
|
+
traits = {},
|
|
163
|
+
context,
|
|
164
|
+
timestamp,
|
|
165
|
+
integrations,
|
|
166
|
+
}: IdentifyParams,
|
|
167
|
+
callback?: Callback
|
|
168
|
+
): void {
|
|
169
|
+
const CustomerioEvent = this._eventFactory.identify(userId, traits, {
|
|
170
|
+
context,
|
|
171
|
+
anonymousId,
|
|
172
|
+
userId,
|
|
173
|
+
timestamp,
|
|
174
|
+
integrations,
|
|
175
|
+
})
|
|
176
|
+
this._dispatch(CustomerioEvent, callback)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The page method lets you record page views on your website, along with optional extra information about the page being viewed.
|
|
181
|
+
*/
|
|
182
|
+
page(
|
|
183
|
+
{
|
|
184
|
+
userId,
|
|
185
|
+
anonymousId,
|
|
186
|
+
category,
|
|
187
|
+
name,
|
|
188
|
+
properties,
|
|
189
|
+
context,
|
|
190
|
+
timestamp,
|
|
191
|
+
integrations,
|
|
192
|
+
}: PageParams,
|
|
193
|
+
callback?: Callback
|
|
194
|
+
): void {
|
|
195
|
+
const CustomerioEvent = this._eventFactory.page(
|
|
196
|
+
category ?? null,
|
|
197
|
+
name ?? null,
|
|
198
|
+
properties,
|
|
199
|
+
{ context, anonymousId, userId, timestamp, integrations }
|
|
200
|
+
)
|
|
201
|
+
this._dispatch(CustomerioEvent, callback)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Records screen views on your app, along with optional extra information
|
|
206
|
+
* about the screen viewed by the user.
|
|
207
|
+
*/
|
|
208
|
+
screen(
|
|
209
|
+
{
|
|
210
|
+
userId,
|
|
211
|
+
anonymousId,
|
|
212
|
+
category,
|
|
213
|
+
name,
|
|
214
|
+
properties,
|
|
215
|
+
context,
|
|
216
|
+
timestamp,
|
|
217
|
+
integrations,
|
|
218
|
+
}: PageParams,
|
|
219
|
+
callback?: Callback
|
|
220
|
+
): void {
|
|
221
|
+
const CustomerioEvent = this._eventFactory.screen(
|
|
222
|
+
category ?? null,
|
|
223
|
+
name ?? null,
|
|
224
|
+
properties,
|
|
225
|
+
{ context, anonymousId, userId, timestamp, integrations }
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
this._dispatch(CustomerioEvent, callback)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Records actions your users perform.
|
|
233
|
+
*/
|
|
234
|
+
track(
|
|
235
|
+
{
|
|
236
|
+
userId,
|
|
237
|
+
anonymousId,
|
|
238
|
+
event,
|
|
239
|
+
properties,
|
|
240
|
+
context,
|
|
241
|
+
timestamp,
|
|
242
|
+
integrations,
|
|
243
|
+
}: TrackParams,
|
|
244
|
+
callback?: Callback
|
|
245
|
+
): void {
|
|
246
|
+
const CustomerioEvent = this._eventFactory.track(event, properties, {
|
|
247
|
+
context,
|
|
248
|
+
userId,
|
|
249
|
+
anonymousId,
|
|
250
|
+
timestamp,
|
|
251
|
+
integrations,
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
this._dispatch(CustomerioEvent, callback)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Registers one or more plugins to augment Analytics functionality.
|
|
259
|
+
* @param plugins
|
|
260
|
+
*/
|
|
261
|
+
register(...plugins: Plugin[]): Promise<void> {
|
|
262
|
+
return this._queue.criticalTasks.run(async () => {
|
|
263
|
+
const ctx = Context.system()
|
|
264
|
+
|
|
265
|
+
const registrations = plugins.map((xt) =>
|
|
266
|
+
this._queue.register(ctx, xt, this)
|
|
267
|
+
)
|
|
268
|
+
await Promise.all(registrations)
|
|
269
|
+
this.emit(
|
|
270
|
+
'register',
|
|
271
|
+
plugins.map((el) => el.name)
|
|
272
|
+
)
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Deregisters one or more plugins based on their names.
|
|
278
|
+
* @param pluginNames - The names of one or more plugins to deregister.
|
|
279
|
+
*/
|
|
280
|
+
async deregister(...pluginNames: string[]): Promise<void> {
|
|
281
|
+
const ctx = Context.system()
|
|
282
|
+
|
|
283
|
+
const deregistrations = pluginNames.map((pl) => {
|
|
284
|
+
const plugin = this._queue.plugins.find((p) => p.name === pl)
|
|
285
|
+
if (plugin) {
|
|
286
|
+
return this._queue.deregister(ctx, plugin, this)
|
|
287
|
+
} else {
|
|
288
|
+
ctx.log('warn', `plugin ${pl} not found`)
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
await Promise.all(deregistrations)
|
|
293
|
+
this.emit('deregister', pluginNames)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// create a derived class since we may want to add node specific things to Context later
|
|
2
|
+
|
|
3
|
+
import { CoreContext } from '@customerio/cdp-analytics-core'
|
|
4
|
+
import { CustomerioEvent } from './types'
|
|
5
|
+
|
|
6
|
+
// While this is not a type, it is a definition
|
|
7
|
+
export class Context extends CoreContext<CustomerioEvent> {
|
|
8
|
+
static override system() {
|
|
9
|
+
return new this({ type: 'track', event: 'system' })
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { dispatch } from '@customerio/cdp-analytics-core'
|
|
2
|
+
import type { NodeEmitter } from './emitter'
|
|
3
|
+
import { Context } from './context'
|
|
4
|
+
import { NodeEventQueue } from './event-queue'
|
|
5
|
+
import { CustomerioEvent } from './types'
|
|
6
|
+
|
|
7
|
+
export type Callback = (err?: unknown, ctx?: Context) => void
|
|
8
|
+
|
|
9
|
+
const normalizeDispatchCb = (cb: Callback) => (ctx: Context) => {
|
|
10
|
+
const failedDelivery = ctx.failedDelivery()
|
|
11
|
+
return failedDelivery ? cb(failedDelivery.reason, ctx) : cb(undefined, ctx)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Dispatch function, but swallow promise rejections and use event emitter instead */
|
|
15
|
+
export const dispatchAndEmit = async (
|
|
16
|
+
event: CustomerioEvent,
|
|
17
|
+
queue: NodeEventQueue,
|
|
18
|
+
emitter: NodeEmitter,
|
|
19
|
+
callback?: Callback
|
|
20
|
+
): Promise<void> => {
|
|
21
|
+
try {
|
|
22
|
+
const context = new Context(event)
|
|
23
|
+
const ctx = await dispatch(context, queue, emitter, {
|
|
24
|
+
...(callback ? { callback: normalizeDispatchCb(callback) } : {}),
|
|
25
|
+
})
|
|
26
|
+
const failedDelivery = ctx.failedDelivery()
|
|
27
|
+
if (failedDelivery) {
|
|
28
|
+
emitter.emit('error', {
|
|
29
|
+
code: 'delivery_failure',
|
|
30
|
+
reason: failedDelivery.reason,
|
|
31
|
+
ctx: ctx,
|
|
32
|
+
})
|
|
33
|
+
} else {
|
|
34
|
+
emitter.emit(event.type, ctx)
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
emitter.emit('error', {
|
|
38
|
+
code: 'unknown',
|
|
39
|
+
reason: err,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CoreEmitterContract, Emitter } from '@customerio/cdp-analytics-core'
|
|
2
|
+
import { Context } from './context'
|
|
3
|
+
import type { AnalyticsSettings } from './settings'
|
|
4
|
+
import { CustomerioEvent } from './types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Map of emitter event names to method args.
|
|
8
|
+
*/
|
|
9
|
+
export type NodeEmitterEvents = CoreEmitterContract<Context> & {
|
|
10
|
+
initialize: [AnalyticsSettings]
|
|
11
|
+
call_after_close: [CustomerioEvent] // any event that did not get dispatched due to close
|
|
12
|
+
http_request: [
|
|
13
|
+
{
|
|
14
|
+
url: string
|
|
15
|
+
method: string
|
|
16
|
+
headers: Record<string, string>
|
|
17
|
+
body: string
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
drained: []
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class NodeEmitter extends Emitter<NodeEmitterEvents> { }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { EventFactory } from '@customerio/cdp-analytics-core'
|
|
2
|
+
import { createMessageId } from '../lib/get-message-id'
|
|
3
|
+
import { CustomerioEvent } from './types'
|
|
4
|
+
|
|
5
|
+
// use declaration merging to downcast CoreCustomerioEvent without adding any runtime code.
|
|
6
|
+
// if/when we decide to add an actual implementation to NodeEventFactory that actually changes the event shape, we can remove this.
|
|
7
|
+
export interface NodeEventFactory {
|
|
8
|
+
alias(...args: Parameters<EventFactory['alias']>): CustomerioEvent
|
|
9
|
+
group(...args: Parameters<EventFactory['group']>): CustomerioEvent
|
|
10
|
+
identify(...args: Parameters<EventFactory['identify']>): CustomerioEvent
|
|
11
|
+
track(...args: Parameters<EventFactory['track']>): CustomerioEvent
|
|
12
|
+
page(...args: Parameters<EventFactory['page']>): CustomerioEvent
|
|
13
|
+
screen(...args: Parameters<EventFactory['screen']>): CustomerioEvent
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class NodeEventFactory extends EventFactory {
|
|
17
|
+
constructor() {
|
|
18
|
+
super({ createMessageId })
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CoreEventQueue, PriorityQueue } from '@customerio/cdp-analytics-core'
|
|
2
|
+
import type { Plugin } from '../app/types'
|
|
3
|
+
import type { Context } from './context'
|
|
4
|
+
|
|
5
|
+
class NodePriorityQueue extends PriorityQueue<Context> {
|
|
6
|
+
constructor() {
|
|
7
|
+
super(1, [])
|
|
8
|
+
}
|
|
9
|
+
// do not use an internal "seen" map
|
|
10
|
+
getAttempts(ctx: Context): number {
|
|
11
|
+
return ctx.attempts ?? 0
|
|
12
|
+
}
|
|
13
|
+
updateAttempts(ctx: Context): number {
|
|
14
|
+
ctx.attempts = this.getAttempts(ctx) + 1
|
|
15
|
+
return this.getAttempts(ctx)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class NodeEventQueue extends CoreEventQueue<Context, Plugin> {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(new NodePriorityQueue())
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ValidationError } from '@customerio/cdp-analytics-core'
|
|
2
|
+
|
|
3
|
+
export interface AnalyticsSettings {
|
|
4
|
+
/**
|
|
5
|
+
* Key for your workspace
|
|
6
|
+
*/
|
|
7
|
+
writeKey: string
|
|
8
|
+
/**
|
|
9
|
+
/**
|
|
10
|
+
* The base URL of the API. Default: "https://cdp.customer.io"
|
|
11
|
+
*/
|
|
12
|
+
host?: string
|
|
13
|
+
/**
|
|
14
|
+
* The API path route. Default: "/v1/batch"
|
|
15
|
+
*/
|
|
16
|
+
path?: string
|
|
17
|
+
/**
|
|
18
|
+
* The number of times to retry flushing a batch. Default: 3
|
|
19
|
+
*/
|
|
20
|
+
maxRetries?: number
|
|
21
|
+
/**
|
|
22
|
+
* The number of messages to enqueue before flushing. Default: 15
|
|
23
|
+
*/
|
|
24
|
+
maxEventsInBatch?: number
|
|
25
|
+
/**
|
|
26
|
+
* The number of milliseconds to wait before flushing the queue automatically. Default: 10000
|
|
27
|
+
*/
|
|
28
|
+
flushInterval?: number
|
|
29
|
+
/**
|
|
30
|
+
* The maximum number of milliseconds to wait for an http request. Default: 10000
|
|
31
|
+
*/
|
|
32
|
+
httpRequestTimeout?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const validateSettings = (settings: AnalyticsSettings) => {
|
|
36
|
+
if (!settings.writeKey) {
|
|
37
|
+
throw new ValidationError('writeKey', 'writeKey is missing.')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GroupTraits,
|
|
3
|
+
UserTraits,
|
|
4
|
+
CoreExtraContext,
|
|
5
|
+
EventProperties,
|
|
6
|
+
Integrations,
|
|
7
|
+
Timestamp,
|
|
8
|
+
} from '@customerio/cdp-analytics-core'
|
|
9
|
+
|
|
10
|
+
export type { GroupTraits, UserTraits }
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A dictionary of extra context to attach to the call.
|
|
14
|
+
* Note: context differs from traits because it is not attributes of the user itself.
|
|
15
|
+
*/
|
|
16
|
+
export interface ExtraContext extends CoreExtraContext { }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* An ID associated with the user. Note: at least one of userId or anonymousId must be included.
|
|
20
|
+
**/
|
|
21
|
+
type IdentityOptions =
|
|
22
|
+
| { userId: string; anonymousId?: string }
|
|
23
|
+
| { userId?: string; anonymousId: string }
|
|
24
|
+
|
|
25
|
+
export type AliasParams = {
|
|
26
|
+
/* The new user id you want to associate with the user. */
|
|
27
|
+
userId: string
|
|
28
|
+
/* The previous id that the user was recognized by (this can be either a userId or an anonymousId). */
|
|
29
|
+
previousId: string
|
|
30
|
+
context?: ExtraContext
|
|
31
|
+
timestamp?: Timestamp
|
|
32
|
+
integrations?: Integrations
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type GroupParams = {
|
|
36
|
+
groupId: string
|
|
37
|
+
/**
|
|
38
|
+
* Traits are pieces of information you know about a group.
|
|
39
|
+
*/
|
|
40
|
+
traits?: GroupTraits
|
|
41
|
+
context?: ExtraContext
|
|
42
|
+
timestamp?: Timestamp
|
|
43
|
+
integrations?: Integrations
|
|
44
|
+
} & IdentityOptions
|
|
45
|
+
|
|
46
|
+
export type IdentifyParams = {
|
|
47
|
+
/**
|
|
48
|
+
* Traits are pieces of information you know about a group.
|
|
49
|
+
*/
|
|
50
|
+
traits?: UserTraits
|
|
51
|
+
context?: ExtraContext
|
|
52
|
+
timestamp?: Timestamp
|
|
53
|
+
integrations?: Integrations
|
|
54
|
+
} & IdentityOptions
|
|
55
|
+
|
|
56
|
+
export type PageParams = {
|
|
57
|
+
/* The category of the page. Useful for cases like ecommerce where many pages might live under a single category. */
|
|
58
|
+
category?: string
|
|
59
|
+
/* The name of the page.*/
|
|
60
|
+
name?: string
|
|
61
|
+
/* A dictionary of properties of the page. */
|
|
62
|
+
properties?: EventProperties
|
|
63
|
+
timestamp?: Timestamp
|
|
64
|
+
context?: ExtraContext
|
|
65
|
+
integrations?: Integrations
|
|
66
|
+
} & IdentityOptions
|
|
67
|
+
|
|
68
|
+
export type TrackParams = {
|
|
69
|
+
event: string
|
|
70
|
+
properties?: EventProperties
|
|
71
|
+
context?: ExtraContext
|
|
72
|
+
timestamp?: Timestamp
|
|
73
|
+
integrations?: Integrations
|
|
74
|
+
} & IdentityOptions
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { Analytics } from './app/analytics-node'
|
|
2
|
+
export { Context } from './app/context'
|
|
3
|
+
export type {
|
|
4
|
+
Plugin,
|
|
5
|
+
GroupTraits,
|
|
6
|
+
UserTraits,
|
|
7
|
+
TrackParams,
|
|
8
|
+
IdentifyParams,
|
|
9
|
+
AliasParams,
|
|
10
|
+
GroupParams,
|
|
11
|
+
PageParams,
|
|
12
|
+
} from './app/types'
|
|
13
|
+
export type { AnalyticsSettings } from './app/settings'
|
|
14
|
+
|
|
15
|
+
// export Analytics as both a named export and a default export (for backwards-compat. reasons)
|
|
16
|
+
import { Analytics } from './app/analytics-node'
|
|
17
|
+
export default Analytics
|
package/src/lib/abort.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* use non-native event emitter for the benefit of non-node runtimes like CF workers.
|
|
3
|
+
*/
|
|
4
|
+
import { Emitter } from '@customerio/cdp-analytics-core'
|
|
5
|
+
import { detectRuntime } from './env'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* adapted from: https://www.npmjs.com/package/node-abort-controller
|
|
9
|
+
*/
|
|
10
|
+
class AbortSignal {
|
|
11
|
+
onabort: globalThis.AbortSignal['onabort'] = null
|
|
12
|
+
aborted = false
|
|
13
|
+
eventEmitter = new Emitter()
|
|
14
|
+
|
|
15
|
+
toString() {
|
|
16
|
+
return '[object AbortSignal]'
|
|
17
|
+
}
|
|
18
|
+
get [Symbol.toStringTag]() {
|
|
19
|
+
return 'AbortSignal'
|
|
20
|
+
}
|
|
21
|
+
removeEventListener(...args: Parameters<Emitter['off']>) {
|
|
22
|
+
this.eventEmitter.off(...args)
|
|
23
|
+
}
|
|
24
|
+
addEventListener(...args: Parameters<Emitter['on']>) {
|
|
25
|
+
this.eventEmitter.on(...args)
|
|
26
|
+
}
|
|
27
|
+
dispatchEvent(type: string) {
|
|
28
|
+
const event = { type, target: this }
|
|
29
|
+
|
|
30
|
+
const handlerName = `on${type}`
|
|
31
|
+
|
|
32
|
+
if (typeof (this as any)[handlerName] === 'function') {
|
|
33
|
+
; (this as any)[handlerName](event)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.eventEmitter.emit(type, event)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* This polyfill is only neccessary to support versions of node < 14.17.
|
|
42
|
+
* Can be removed once node 14 support is dropped.
|
|
43
|
+
*/
|
|
44
|
+
class AbortController {
|
|
45
|
+
signal = new AbortSignal()
|
|
46
|
+
abort() {
|
|
47
|
+
if (this.signal.aborted) return
|
|
48
|
+
|
|
49
|
+
this.signal.aborted = true
|
|
50
|
+
this.signal.dispatchEvent('abort')
|
|
51
|
+
}
|
|
52
|
+
toString() {
|
|
53
|
+
return '[object AbortController]'
|
|
54
|
+
}
|
|
55
|
+
get [Symbol.toStringTag]() {
|
|
56
|
+
return 'AbortController'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param timeoutMs - Set a request timeout, after which the request is cancelled.
|
|
62
|
+
*/
|
|
63
|
+
export const abortSignalAfterTimeout = (timeoutMs: number) => {
|
|
64
|
+
if (detectRuntime() === 'cloudflare-worker') {
|
|
65
|
+
return [] // TODO: this is broken in cloudflare workers, otherwise results in "A hanging Promise was canceled..." error.
|
|
66
|
+
}
|
|
67
|
+
const ac = new (global.AbortController || AbortController)()
|
|
68
|
+
|
|
69
|
+
const timeoutId = setTimeout(() => {
|
|
70
|
+
ac.abort()
|
|
71
|
+
}, timeoutMs)
|
|
72
|
+
|
|
73
|
+
// Allow Node.js processes to exit early if only the timeout is running
|
|
74
|
+
timeoutId?.unref?.()
|
|
75
|
+
|
|
76
|
+
return [ac.signal, timeoutId] as const
|
|
77
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 encoder that works in browser, worker, node runtimes.
|
|
3
|
+
*/
|
|
4
|
+
export const b64encode = (str: string): string => {
|
|
5
|
+
if (
|
|
6
|
+
// in node env
|
|
7
|
+
typeof Buffer !== 'undefined'
|
|
8
|
+
) {
|
|
9
|
+
return Buffer.from(str).toString('base64')
|
|
10
|
+
} else {
|
|
11
|
+
// in worker env
|
|
12
|
+
return btoa(str)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const stripTrailingSlash = (str: string) => str.replace(/\/$/, '')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @param host e.g. "http://foo.com"
|
|
6
|
+
* @param path e.g. "/bar"
|
|
7
|
+
* @returns "e.g." "http://foo.com/bar"
|
|
8
|
+
*/
|
|
9
|
+
export const tryCreateFormattedUrl = (host: string, path?: string) => {
|
|
10
|
+
return stripTrailingSlash(new URL(path || '', host).href)
|
|
11
|
+
}
|