@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.
Files changed (171) hide show
  1. package/LICENSE.MD +22 -0
  2. package/README.md +20 -0
  3. package/dist/cjs/app/analytics-node.js +170 -0
  4. package/dist/cjs/app/analytics-node.js.map +1 -0
  5. package/dist/cjs/app/context.js +13 -0
  6. package/dist/cjs/app/context.js.map +1 -0
  7. package/dist/cjs/app/dispatch-emit.js +37 -0
  8. package/dist/cjs/app/dispatch-emit.js.map +1 -0
  9. package/dist/cjs/app/emitter.js +8 -0
  10. package/dist/cjs/app/emitter.js.map +1 -0
  11. package/dist/cjs/app/event-factory.js +12 -0
  12. package/dist/cjs/app/event-factory.js.map +1 -0
  13. package/dist/cjs/app/event-queue.js +24 -0
  14. package/dist/cjs/app/event-queue.js.map +1 -0
  15. package/dist/cjs/app/settings.js +11 -0
  16. package/dist/cjs/app/settings.js.map +1 -0
  17. package/dist/cjs/app/types/event.js +3 -0
  18. package/dist/cjs/app/types/event.js.map +1 -0
  19. package/dist/cjs/app/types/index.js +7 -0
  20. package/dist/cjs/app/types/index.js.map +1 -0
  21. package/dist/cjs/app/types/params.js +3 -0
  22. package/dist/cjs/app/types/params.js.map +1 -0
  23. package/dist/cjs/app/types/plugin.js +3 -0
  24. package/dist/cjs/app/types/plugin.js.map +1 -0
  25. package/dist/cjs/generated/version.js +6 -0
  26. package/dist/cjs/generated/version.js.map +1 -0
  27. package/dist/cjs/index.js +11 -0
  28. package/dist/cjs/index.js.map +1 -0
  29. package/dist/cjs/lib/abort.js +77 -0
  30. package/dist/cjs/lib/abort.js.map +1 -0
  31. package/dist/cjs/lib/base-64-encode.js +19 -0
  32. package/dist/cjs/lib/base-64-encode.js.map +1 -0
  33. package/dist/cjs/lib/create-url.js +15 -0
  34. package/dist/cjs/lib/create-url.js.map +1 -0
  35. package/dist/cjs/lib/env.js +25 -0
  36. package/dist/cjs/lib/env.js.map +1 -0
  37. package/dist/cjs/lib/extract-promise-parts.js +21 -0
  38. package/dist/cjs/lib/extract-promise-parts.js.map +1 -0
  39. package/dist/cjs/lib/fetch.js +7 -0
  40. package/dist/cjs/lib/fetch.js.map +1 -0
  41. package/dist/cjs/lib/get-message-id.js +14 -0
  42. package/dist/cjs/lib/get-message-id.js.map +1 -0
  43. package/dist/cjs/lib/uuid.js +6 -0
  44. package/dist/cjs/lib/uuid.js.map +1 -0
  45. package/dist/cjs/plugins/customerio/context-batch.js +55 -0
  46. package/dist/cjs/plugins/customerio/context-batch.js.map +1 -0
  47. package/dist/cjs/plugins/customerio/index.js +44 -0
  48. package/dist/cjs/plugins/customerio/index.js.map +1 -0
  49. package/dist/cjs/plugins/customerio/publisher.js +189 -0
  50. package/dist/cjs/plugins/customerio/publisher.js.map +1 -0
  51. package/dist/esm/app/analytics-node.js +166 -0
  52. package/dist/esm/app/analytics-node.js.map +1 -0
  53. package/dist/esm/app/context.js +9 -0
  54. package/dist/esm/app/context.js.map +1 -0
  55. package/dist/esm/app/dispatch-emit.js +33 -0
  56. package/dist/esm/app/dispatch-emit.js.map +1 -0
  57. package/dist/esm/app/emitter.js +4 -0
  58. package/dist/esm/app/emitter.js.map +1 -0
  59. package/dist/esm/app/event-factory.js +8 -0
  60. package/dist/esm/app/event-factory.js.map +1 -0
  61. package/dist/esm/app/event-queue.js +20 -0
  62. package/dist/esm/app/event-queue.js.map +1 -0
  63. package/dist/esm/app/settings.js +7 -0
  64. package/dist/esm/app/settings.js.map +1 -0
  65. package/dist/esm/app/types/event.js +2 -0
  66. package/dist/esm/app/types/event.js.map +1 -0
  67. package/dist/esm/app/types/index.js +4 -0
  68. package/dist/esm/app/types/index.js.map +1 -0
  69. package/dist/esm/app/types/params.js +2 -0
  70. package/dist/esm/app/types/params.js.map +1 -0
  71. package/dist/esm/app/types/plugin.js +2 -0
  72. package/dist/esm/app/types/plugin.js.map +1 -0
  73. package/dist/esm/generated/version.js +3 -0
  74. package/dist/esm/generated/version.js.map +1 -0
  75. package/dist/esm/index.js +6 -0
  76. package/dist/esm/index.js.map +1 -0
  77. package/dist/esm/lib/abort.js +73 -0
  78. package/dist/esm/lib/abort.js.map +1 -0
  79. package/dist/esm/lib/base-64-encode.js +15 -0
  80. package/dist/esm/lib/base-64-encode.js.map +1 -0
  81. package/dist/esm/lib/create-url.js +11 -0
  82. package/dist/esm/lib/create-url.js.map +1 -0
  83. package/dist/esm/lib/env.js +21 -0
  84. package/dist/esm/lib/env.js.map +1 -0
  85. package/dist/esm/lib/extract-promise-parts.js +17 -0
  86. package/dist/esm/lib/extract-promise-parts.js.map +1 -0
  87. package/dist/esm/lib/fetch.js +3 -0
  88. package/dist/esm/lib/fetch.js.map +1 -0
  89. package/dist/esm/lib/get-message-id.js +10 -0
  90. package/dist/esm/lib/get-message-id.js.map +1 -0
  91. package/dist/esm/lib/uuid.js +2 -0
  92. package/dist/esm/lib/uuid.js.map +1 -0
  93. package/dist/esm/plugins/customerio/context-batch.js +51 -0
  94. package/dist/esm/plugins/customerio/context-batch.js.map +1 -0
  95. package/dist/esm/plugins/customerio/index.js +39 -0
  96. package/dist/esm/plugins/customerio/index.js.map +1 -0
  97. package/dist/esm/plugins/customerio/publisher.js +185 -0
  98. package/dist/esm/plugins/customerio/publisher.js.map +1 -0
  99. package/dist/types/app/analytics-node.d.ts +62 -0
  100. package/dist/types/app/analytics-node.d.ts.map +1 -0
  101. package/dist/types/app/context.d.ts +6 -0
  102. package/dist/types/app/context.d.ts.map +1 -0
  103. package/dist/types/app/dispatch-emit.d.ts +7 -0
  104. package/dist/types/app/dispatch-emit.d.ts.map +1 -0
  105. package/dist/types/app/emitter.d.ts +23 -0
  106. package/dist/types/app/emitter.d.ts.map +1 -0
  107. package/dist/types/app/event-factory.d.ts +14 -0
  108. package/dist/types/app/event-factory.d.ts.map +1 -0
  109. package/dist/types/app/event-queue.d.ts +7 -0
  110. package/dist/types/app/event-queue.d.ts.map +1 -0
  111. package/dist/types/app/settings.d.ts +33 -0
  112. package/dist/types/app/settings.d.ts.map +1 -0
  113. package/dist/types/app/types/event.d.ts +7 -0
  114. package/dist/types/app/types/event.d.ts.map +1 -0
  115. package/dist/types/app/types/index.d.ts +4 -0
  116. package/dist/types/app/types/index.d.ts.map +1 -0
  117. package/dist/types/app/types/params.d.ts +60 -0
  118. package/dist/types/app/types/params.d.ts.map +1 -0
  119. package/dist/types/app/types/plugin.d.ts +6 -0
  120. package/dist/types/app/types/plugin.d.ts.map +1 -0
  121. package/dist/types/generated/version.d.ts +2 -0
  122. package/dist/types/generated/version.d.ts.map +1 -0
  123. package/dist/types/index.d.ts +7 -0
  124. package/dist/types/index.d.ts.map +1 -0
  125. package/dist/types/lib/abort.d.ts +6 -0
  126. package/dist/types/lib/abort.d.ts.map +1 -0
  127. package/dist/types/lib/base-64-encode.d.ts +5 -0
  128. package/dist/types/lib/base-64-encode.d.ts.map +1 -0
  129. package/dist/types/lib/create-url.d.ts +8 -0
  130. package/dist/types/lib/create-url.d.ts.map +1 -0
  131. package/dist/types/lib/env.d.ts +3 -0
  132. package/dist/types/lib/env.d.ts.map +1 -0
  133. package/dist/types/lib/extract-promise-parts.d.ts +9 -0
  134. package/dist/types/lib/extract-promise-parts.d.ts.map +1 -0
  135. package/dist/types/lib/fetch.d.ts +2 -0
  136. package/dist/types/lib/fetch.d.ts.map +1 -0
  137. package/dist/types/lib/get-message-id.d.ts +7 -0
  138. package/dist/types/lib/get-message-id.d.ts.map +1 -0
  139. package/dist/types/lib/uuid.d.ts +2 -0
  140. package/dist/types/lib/uuid.d.ts.map +1 -0
  141. package/dist/types/plugins/customerio/context-batch.d.ts +26 -0
  142. package/dist/types/plugins/customerio/context-batch.d.ts.map +1 -0
  143. package/dist/types/plugins/customerio/index.d.ts +13 -0
  144. package/dist/types/plugins/customerio/index.d.ts.map +1 -0
  145. package/dist/types/plugins/customerio/publisher.d.ts +38 -0
  146. package/dist/types/plugins/customerio/publisher.d.ts.map +1 -0
  147. package/package.json +43 -0
  148. package/src/app/analytics-node.ts +295 -0
  149. package/src/app/context.ts +11 -0
  150. package/src/app/dispatch-emit.ts +42 -0
  151. package/src/app/emitter.ts +23 -0
  152. package/src/app/event-factory.ts +20 -0
  153. package/src/app/event-queue.ts +23 -0
  154. package/src/app/settings.ts +39 -0
  155. package/src/app/types/event.ts +7 -0
  156. package/src/app/types/index.ts +3 -0
  157. package/src/app/types/params.ts +74 -0
  158. package/src/app/types/plugin.ts +5 -0
  159. package/src/generated/version.ts +2 -0
  160. package/src/index.ts +17 -0
  161. package/src/lib/abort.ts +77 -0
  162. package/src/lib/base-64-encode.ts +14 -0
  163. package/src/lib/create-url.ts +11 -0
  164. package/src/lib/env.ts +32 -0
  165. package/src/lib/extract-promise-parts.ts +21 -0
  166. package/src/lib/fetch.ts +3 -0
  167. package/src/lib/get-message-id.ts +10 -0
  168. package/src/lib/uuid.ts +1 -0
  169. package/src/plugins/customerio/context-batch.ts +71 -0
  170. package/src/plugins/customerio/index.ts +65 -0
  171. package/src/plugins/customerio/publisher.ts +258 -0
package/src/lib/env.ts ADDED
@@ -0,0 +1,32 @@
1
+ export type RuntimeEnv =
2
+ | 'node'
3
+ | 'browser'
4
+ | 'web-worker'
5
+ | 'cloudflare-worker'
6
+ | 'unknown'
7
+
8
+ export const detectRuntime = (): RuntimeEnv => {
9
+ if (typeof process === 'object' && process && process.env) {
10
+ return 'node'
11
+ }
12
+
13
+ if (typeof window === 'object') {
14
+ return 'browser'
15
+ }
16
+
17
+ // @ts-ignore
18
+ if (typeof WebSocketPair !== 'undefined') {
19
+ return 'cloudflare-worker'
20
+ }
21
+
22
+ if (
23
+ // @ts-ignore
24
+ typeof WorkerGlobalScope !== 'undefined' &&
25
+ // @ts-ignore
26
+ typeof importScripts === 'function'
27
+ ) {
28
+ return 'web-worker'
29
+ }
30
+
31
+ return 'unknown'
32
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Returns a promise and its associated `resolve` and `reject` methods.
3
+ */
4
+ export function extractPromiseParts<T = unknown>(): {
5
+ promise: Promise<T>
6
+ resolve: (value: T) => void
7
+ reject: (reason?: unknown) => void
8
+ } {
9
+ let resolver: (value: T) => void
10
+ let rejecter: (reason?: unknown) => void
11
+ const promise = new Promise<T>((resolve, reject) => {
12
+ resolver = resolve
13
+ rejecter = reject
14
+ })
15
+
16
+ return {
17
+ promise,
18
+ resolve: resolver!,
19
+ reject: rejecter!,
20
+ }
21
+ }
@@ -0,0 +1,3 @@
1
+ import { default as _fetch } from 'node-fetch'
2
+
3
+ export const fetch = globalThis.fetch || _fetch
@@ -0,0 +1,10 @@
1
+ import { uuid } from './uuid'
2
+
3
+ /**
4
+ * get a unique messageId with a very low chance of collisions
5
+ * using @lukeed/uuid/secure uses the node crypto module, which is the fastest
6
+ * @example "node-next-1668208232027-743be593-7789-4b74-8078-cbcc8894c586"
7
+ */
8
+ export const createMessageId = (): string => {
9
+ return `node-next-${Date.now()}-${uuid()}`
10
+ }
@@ -0,0 +1 @@
1
+ export { v4 as uuid } from 'uuid'
@@ -0,0 +1,71 @@
1
+ import { v4 as uuid } from '@lukeed/uuid'
2
+ import type { Context } from '../../app/context'
3
+ import { CustomerioEvent } from '../../app/types'
4
+
5
+ const MAX_EVENT_SIZE_IN_KB = 32
6
+ const MAX_BATCH_SIZE_IN_KB = 480 // (500 KB is the limit, leaving some padding)
7
+
8
+ interface PendingItem {
9
+ resolver: (ctx: Context) => void
10
+ context: Context
11
+ }
12
+
13
+ export class ContextBatch {
14
+ public id = uuid()
15
+ private items: PendingItem[] = []
16
+ private sizeInBytes = 0
17
+ private maxEventCount: number
18
+
19
+ constructor(maxEventCount: number) {
20
+ this.maxEventCount = Math.max(1, maxEventCount)
21
+ }
22
+ public tryAdd(
23
+ item: PendingItem
24
+ ): { success: true } | { success: false; message: string } {
25
+ if (this.length === this.maxEventCount)
26
+ return {
27
+ success: false,
28
+ message: `Event limit of ${this.maxEventCount} has been exceeded.`,
29
+ }
30
+
31
+ const eventSize = this.calculateSize(item.context)
32
+ if (eventSize > MAX_EVENT_SIZE_IN_KB * 1024) {
33
+ return {
34
+ success: false,
35
+ message: `Event exceeds maximum event size of ${MAX_EVENT_SIZE_IN_KB} KB`,
36
+ }
37
+ }
38
+
39
+ if (this.sizeInBytes + eventSize > MAX_BATCH_SIZE_IN_KB * 1024) {
40
+ return {
41
+ success: false,
42
+ message: `Event has caused batch size to exceed ${MAX_BATCH_SIZE_IN_KB} KB`,
43
+ }
44
+ }
45
+
46
+ this.items.push(item)
47
+ this.sizeInBytes += eventSize
48
+ return { success: true }
49
+ }
50
+
51
+ get length(): number {
52
+ return this.items.length
53
+ }
54
+
55
+ private calculateSize(ctx: Context): number {
56
+ return encodeURI(JSON.stringify(ctx.event)).split(/%..|i/).length
57
+ }
58
+
59
+ getEvents(): CustomerioEvent[] {
60
+ const events = this.items.map(({ context }) => context.event)
61
+ return events
62
+ }
63
+
64
+ getContexts(): Context[] {
65
+ return this.items.map((item) => item.context)
66
+ }
67
+
68
+ resolveEvents(): void {
69
+ this.items.forEach(({ resolver, context }) => resolver(context))
70
+ }
71
+ }
@@ -0,0 +1,65 @@
1
+ import { Publisher, PublisherProps } from './publisher'
2
+ import { version } from '../../generated/version'
3
+ import { detectRuntime } from '../../lib/env'
4
+ import { Plugin } from '../../app/types'
5
+ import { Context } from '../../app/context'
6
+ import { NodeEmitter } from '../../app/emitter'
7
+
8
+ function normalizeEvent(ctx: Context) {
9
+ ctx.updateEvent('context.library.name', '@customerio/cdp-analytics-node')
10
+ ctx.updateEvent('context.library.version', version)
11
+ const runtime = detectRuntime()
12
+ if (runtime === 'node') {
13
+ ctx.updateEvent('_metadata.nodeVersion', process.versions.node)
14
+ }
15
+ ctx.updateEvent('_metadata.jsRuntime', runtime)
16
+ }
17
+
18
+ type DefinedPluginFields =
19
+ | 'name'
20
+ | 'type'
21
+ | 'version'
22
+ | 'isLoaded'
23
+ | 'load'
24
+ | 'alias'
25
+ | 'group'
26
+ | 'identify'
27
+ | 'page'
28
+ | 'screen'
29
+ | 'track'
30
+
31
+ type CustomerioNodePlugin = Plugin & Required<Pick<Plugin, DefinedPluginFields>>
32
+
33
+ export type ConfigureNodePluginProps = PublisherProps
34
+
35
+ export function createNodePlugin(publisher: Publisher): CustomerioNodePlugin {
36
+ function action(ctx: Context): Promise<Context> {
37
+ normalizeEvent(ctx)
38
+ return publisher.enqueue(ctx)
39
+ }
40
+
41
+ return {
42
+ name: 'Customer.io Data Pipelines',
43
+ type: 'after',
44
+ version: '1.0.0',
45
+ isLoaded: () => true,
46
+ load: () => Promise.resolve(),
47
+ alias: action,
48
+ group: action,
49
+ identify: action,
50
+ page: action,
51
+ screen: action,
52
+ track: action,
53
+ }
54
+ }
55
+
56
+ export const createConfiguredNodePlugin = (
57
+ props: ConfigureNodePluginProps,
58
+ emitter: NodeEmitter
59
+ ) => {
60
+ const publisher = new Publisher(props, emitter)
61
+ return {
62
+ publisher: publisher,
63
+ plugin: createNodePlugin(publisher),
64
+ }
65
+ }
@@ -0,0 +1,258 @@
1
+ import { backoff } from '@customerio/cdp-analytics-core'
2
+ import { abortSignalAfterTimeout } from '../../lib/abort'
3
+ import type { Context } from '../../app/context'
4
+ import { tryCreateFormattedUrl } from '../../lib/create-url'
5
+ import { extractPromiseParts } from '../../lib/extract-promise-parts'
6
+ import { fetch } from '../../lib/fetch'
7
+ import { ContextBatch } from './context-batch'
8
+ import { NodeEmitter } from '../../app/emitter'
9
+ import { b64encode } from '../../lib/base-64-encode'
10
+
11
+ function sleep(timeoutInMs: number): Promise<void> {
12
+ return new Promise((resolve) => setTimeout(resolve, timeoutInMs))
13
+ }
14
+
15
+ function noop() { }
16
+
17
+ interface PendingItem {
18
+ resolver: (ctx: Context) => void
19
+ context: Context
20
+ }
21
+
22
+ export interface PublisherProps {
23
+ host?: string
24
+ path?: string
25
+ flushInterval: number
26
+ maxEventsInBatch: number
27
+ maxRetries: number
28
+ writeKey: string
29
+ httpRequestTimeout?: number
30
+ }
31
+
32
+ /**
33
+ * The Publisher is responsible for batching events and sending them to the API.
34
+ */
35
+ export class Publisher {
36
+ private pendingFlushTimeout?: ReturnType<typeof setTimeout>
37
+ private _batch?: ContextBatch
38
+
39
+ private _flushInterval: number
40
+ private _maxEventsInBatch: number
41
+ private _maxRetries: number
42
+ private _auth: string
43
+ private _url: string
44
+ private _closeAndFlushPendingItemsCount?: number
45
+ private _httpRequestTimeout: number
46
+ private _emitter: NodeEmitter
47
+ constructor(
48
+ {
49
+ host,
50
+ path,
51
+ maxRetries,
52
+ maxEventsInBatch,
53
+ flushInterval,
54
+ writeKey,
55
+ httpRequestTimeout,
56
+ }: PublisherProps,
57
+ emitter: NodeEmitter
58
+ ) {
59
+ this._emitter = emitter
60
+ this._maxRetries = maxRetries
61
+ this._maxEventsInBatch = Math.max(maxEventsInBatch, 1)
62
+ this._flushInterval = flushInterval
63
+ this._auth = b64encode(`${writeKey}:`)
64
+ this._url = tryCreateFormattedUrl(
65
+ host ?? 'https://cdp.customer.io',
66
+ path ?? '/v1/batch'
67
+ )
68
+ this._httpRequestTimeout = httpRequestTimeout ?? 10000
69
+ }
70
+
71
+ private createBatch(): ContextBatch {
72
+ this.pendingFlushTimeout && clearTimeout(this.pendingFlushTimeout)
73
+ const batch = new ContextBatch(this._maxEventsInBatch)
74
+ this._batch = batch
75
+ this.pendingFlushTimeout = setTimeout(() => {
76
+ if (batch === this._batch) {
77
+ this._batch = undefined
78
+ }
79
+ this.pendingFlushTimeout = undefined
80
+ if (batch.length) {
81
+ this.send(batch).catch(noop)
82
+ }
83
+ }, this._flushInterval)
84
+ return batch
85
+ }
86
+
87
+ private clearBatch() {
88
+ this.pendingFlushTimeout && clearTimeout(this.pendingFlushTimeout)
89
+ this._batch = undefined
90
+ }
91
+
92
+ flushAfterClose(pendingItemsCount: number) {
93
+ if (!pendingItemsCount) {
94
+ // if number of pending items is 0, there will never be anything else entering the batch, since the app is closed.
95
+ return
96
+ }
97
+
98
+ this._closeAndFlushPendingItemsCount = pendingItemsCount
99
+
100
+ // if batch is empty, there's nothing to flush, and when things come in, enqueue will handle them.
101
+ if (!this._batch) return
102
+
103
+ // the number of globally pending items will always be larger or the same as batch size.
104
+ // Any mismatch is because some globally pending items are in plugins.
105
+ const isExpectingNoMoreItems = this._batch.length === pendingItemsCount
106
+ if (isExpectingNoMoreItems) {
107
+ this.send(this._batch).catch(noop)
108
+ this.clearBatch()
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Enqueues the context for future delivery.
114
+ * @param ctx - Context containing an event.
115
+ * @returns a promise that resolves with the context after the event has been delivered.
116
+ */
117
+ enqueue(ctx: Context): Promise<Context> {
118
+ const batch = this._batch ?? this.createBatch()
119
+
120
+ const { promise: ctxPromise, resolve } = extractPromiseParts<Context>()
121
+
122
+ const pendingItem: PendingItem = {
123
+ context: ctx,
124
+ resolver: resolve,
125
+ }
126
+
127
+ /*
128
+ The following logic ensures that a batch is never orphaned,
129
+ and is always sent before a new batch is created.
130
+
131
+ Add an event to the existing batch.
132
+ Success: Check if batch is full or no more items are expected to come in (i.e. closing). If so, send batch.
133
+ Failure: Assume event is too big to fit in current batch - send existing batch.
134
+ Add an event to the new batch.
135
+ Success: Check if batch is full and send if it is.
136
+ Failure: Event exceeds maximum size (it will never fit), fail the event.
137
+ */
138
+ const addStatus = batch.tryAdd(pendingItem)
139
+ if (addStatus.success) {
140
+ const isExpectingNoMoreItems =
141
+ batch.length === this._closeAndFlushPendingItemsCount
142
+ const isFull = batch.length === this._maxEventsInBatch
143
+ if (isFull || isExpectingNoMoreItems) {
144
+ this.send(batch).catch(noop)
145
+ this.clearBatch()
146
+ }
147
+ return ctxPromise
148
+ }
149
+
150
+ // If the new item causes the maximimum event size to be exceeded, send the current batch and create a new one.
151
+ if (batch.length) {
152
+ this.send(batch).catch(noop)
153
+ this.clearBatch()
154
+ }
155
+
156
+ const fallbackBatch = this.createBatch()
157
+
158
+ const fbAddStatus = fallbackBatch.tryAdd(pendingItem)
159
+
160
+ if (fbAddStatus.success) {
161
+ const isExpectingNoMoreItems =
162
+ fallbackBatch.length === this._closeAndFlushPendingItemsCount
163
+ if (isExpectingNoMoreItems) {
164
+ this.send(fallbackBatch).catch(noop)
165
+ this.clearBatch()
166
+ }
167
+ return ctxPromise
168
+ } else {
169
+ // this should only occur if max event size is exceeded
170
+ ctx.setFailedDelivery({
171
+ reason: new Error(fbAddStatus.message),
172
+ })
173
+ return Promise.resolve(ctx)
174
+ }
175
+ }
176
+
177
+ private async send(batch: ContextBatch) {
178
+ if (this._closeAndFlushPendingItemsCount) {
179
+ this._closeAndFlushPendingItemsCount -= batch.length
180
+ }
181
+ const events = batch.getEvents()
182
+ const payload = JSON.stringify({ batch: events })
183
+ const maxAttempts = this._maxRetries + 1
184
+
185
+ let currentAttempt = 0
186
+ while (currentAttempt < maxAttempts) {
187
+ currentAttempt++
188
+
189
+ let failureReason: unknown
190
+ const [signal, timeoutId] = abortSignalAfterTimeout(
191
+ this._httpRequestTimeout
192
+ )
193
+ try {
194
+ const requestInit = {
195
+ signal: signal,
196
+ method: 'POST',
197
+ headers: {
198
+ 'Content-Type': 'application/json',
199
+ Authorization: `Basic ${this._auth}`,
200
+ 'User-Agent': 'cdp-analytics-node/latest',
201
+ },
202
+ body: payload,
203
+ }
204
+
205
+ this._emitter.emit('http_request', {
206
+ url: this._url,
207
+ method: requestInit.method,
208
+ headers: requestInit.headers,
209
+ body: requestInit.body,
210
+ })
211
+ const response = await fetch(this._url, requestInit)
212
+
213
+ clearTimeout(timeoutId)
214
+
215
+ if (response.ok) {
216
+ // Successfully sent events, so exit!
217
+ batch.resolveEvents()
218
+ return
219
+ } else if (response.status === 400) {
220
+ // Request either malformed or size exceeded - don't retry.
221
+ resolveFailedBatch(
222
+ batch,
223
+ new Error(`[${response.status}] ${response.statusText}`)
224
+ )
225
+ return
226
+ } else {
227
+ // Treat other errors as transient and retry.
228
+ failureReason = new Error(
229
+ `[${response.status}] ${response.statusText}`
230
+ )
231
+ }
232
+ } catch (err) {
233
+ // Network errors get thrown, retry them.
234
+ failureReason = err
235
+ }
236
+
237
+ // Final attempt failed, update context and resolve events.
238
+ if (currentAttempt === maxAttempts) {
239
+ resolveFailedBatch(batch, failureReason)
240
+ return
241
+ }
242
+
243
+ // Retry after attempt-based backoff.
244
+ await sleep(
245
+ backoff({
246
+ attempt: currentAttempt,
247
+ minTimeout: 25,
248
+ maxTimeout: 1000,
249
+ })
250
+ )
251
+ }
252
+ }
253
+ }
254
+
255
+ function resolveFailedBatch(batch: ContextBatch, reason: unknown) {
256
+ batch.getContexts().forEach((ctx) => ctx.setFailedDelivery({ reason }))
257
+ batch.resolveEvents()
258
+ }