1+ import { fetch as defaultFetch } from '@whatwg-node/fetch' ;
12import { version } from '../version.js' ;
23import { http } from './http-client.js' ;
34import type { Logger } from './types.js' ;
5+ import { CircuitBreakerInterface , createHiveLogger , loadCircuitBreaker } from './utils.js' ;
46
57type ReadOnlyResponse = Pick < Response , 'status' | 'text' | 'json' | 'statusText' > ;
68
9+ export type AgentCircuitBreakerConfiguration = {
10+ /**
11+ * Percentage after what the circuit breaker should kick in.
12+ * Default: 50
13+ */
14+ errorThresholdPercentage : number ;
15+ /**
16+ * Count of requests before starting evaluating.
17+ * Default: 5
18+ */
19+ volumeThreshold : number ;
20+ /**
21+ * After what time the circuit breaker is attempting to retry sending requests in milliseconds
22+ * Default: 30_000
23+ */
24+ resetTimeout : number ;
25+ } ;
26+
27+ const defaultCircuitBreakerConfiguration : AgentCircuitBreakerConfiguration = {
28+ errorThresholdPercentage : 50 ,
29+ volumeThreshold : 10 ,
30+ resetTimeout : 30_000 ,
31+ } ;
32+
733export interface AgentOptions {
834 enabled ?: boolean ;
935 name ?: string ;
@@ -48,7 +74,14 @@ export interface AgentOptions {
4874 * WHATWG Compatible fetch implementation
4975 * used by the agent to send reports
5076 */
51- fetch ?: typeof fetch ;
77+ fetch ?: typeof defaultFetch ;
78+ /**
79+ * Circuit Breaker Configuration.
80+ * true -> Use default configuration
81+ * false -> Disable
82+ * object -> use custom configuration see {AgentCircuitBreakerConfiguration}
83+ */
84+ circuitBreaker ?: boolean | AgentCircuitBreakerConfiguration ;
5285}
5386
5487export function createAgent < TEvent > (
@@ -67,23 +100,31 @@ export function createAgent<TEvent>(
67100 headers ?( ) : Record < string , string > ;
68101 } ,
69102) {
70- const options : Required < Omit < AgentOptions , 'fetch' > > = {
103+ const options : Required < Omit < AgentOptions , 'fetch' | 'circuitBreaker' > > & {
104+ circuitBreaker : null | AgentCircuitBreakerConfiguration ;
105+ } = {
71106 timeout : 30_000 ,
72107 debug : false ,
73108 enabled : true ,
74109 minTimeout : 200 ,
75110 maxRetries : 3 ,
76111 sendInterval : 10_000 ,
77112 maxSize : 25 ,
78- logger : console ,
79113 name : 'hive-client' ,
80114 version,
81115 ...pluginOptions ,
116+ circuitBreaker :
117+ pluginOptions . circuitBreaker == null || pluginOptions . circuitBreaker === true
118+ ? defaultCircuitBreakerConfiguration
119+ : pluginOptions . circuitBreaker === false
120+ ? null
121+ : pluginOptions . circuitBreaker ,
122+ logger : createHiveLogger ( pluginOptions . logger ?? console , '[agent]' ) ,
82123 } ;
83124
84125 const enabled = options . enabled !== false ;
85126
86- let timeoutID : any = null ;
127+ let timeoutID : ReturnType < typeof setTimeout > | null = null ;
87128
88129 function schedule ( ) {
89130 if ( timeoutID ) {
@@ -143,6 +184,27 @@ export function createAgent<TEvent>(
143184 return send ( { throwOnError : true , skipSchedule : true } ) ;
144185 }
145186
187+ async function sendHTTPCall ( buffer : string | Buffer < ArrayBufferLike > ) : Promise < Response > {
188+ const signal = breaker . getSignal ( ) ;
189+ return await http . post ( options . endpoint , buffer , {
190+ headers : {
191+ accept : 'application/json' ,
192+ 'content-type' : 'application/json' ,
193+ Authorization : `Bearer ${ options . token } ` ,
194+ 'User-Agent' : `${ options . name } /${ options . version } ` ,
195+ ...headers ( ) ,
196+ } ,
197+ timeout : options . timeout ,
198+ retry : {
199+ retries : options . maxRetries ,
200+ factor : 2 ,
201+ } ,
202+ logger : options . logger ,
203+ fetchImplementation : pluginOptions . fetch ,
204+ signal,
205+ } ) ;
206+ }
207+
146208 async function send ( sendOptions ?: {
147209 throwOnError ?: boolean ;
148210 skipSchedule : boolean ;
@@ -160,23 +222,7 @@ export function createAgent<TEvent>(
160222 data . clear ( ) ;
161223
162224 debugLog ( `Sending report (queue ${ dataToSend } )` ) ;
163- const response = await http
164- . post ( options . endpoint , buffer , {
165- headers : {
166- accept : 'application/json' ,
167- 'content-type' : 'application/json' ,
168- Authorization : `Bearer ${ options . token } ` ,
169- 'User-Agent' : `${ options . name } /${ options . version } ` ,
170- ...headers ( ) ,
171- } ,
172- timeout : options . timeout ,
173- retry : {
174- retries : options . maxRetries ,
175- factor : 2 ,
176- } ,
177- logger : options . logger ,
178- fetchImplementation : pluginOptions . fetch ,
179- } )
225+ const response = sendFromBreaker ( buffer )
180226 . then ( res => {
181227 debugLog ( `Report sent!` ) ;
182228 return res ;
@@ -215,6 +261,74 @@ export function createAgent<TEvent>(
215261 } ) ;
216262 }
217263
264+ let breaker : CircuitBreakerInterface <
265+ Parameters < typeof sendHTTPCall > ,
266+ ReturnType < typeof sendHTTPCall >
267+ > ;
268+ let loadCircuitBreakerPromise : Promise < void > | null = null ;
269+ const breakerLogger = createHiveLogger ( options . logger , '[circuit breaker]' ) ;
270+
271+ function noopBreaker ( ) : typeof breaker {
272+ return {
273+ getSignal ( ) {
274+ return undefined ;
275+ } ,
276+ fire : sendHTTPCall ,
277+ } ;
278+ }
279+
280+ if ( options . circuitBreaker ) {
281+ /**
282+ * We support Cloudflare, which does not has the `events` module.
283+ * So we lazy load opossum which has `events` as a dependency.
284+ */
285+ breakerLogger . info ( 'initialize circuit breaker' ) ;
286+ loadCircuitBreakerPromise = loadCircuitBreaker (
287+ CircuitBreaker => {
288+ breakerLogger . info ( 'started' ) ;
289+ const realBreaker = new CircuitBreaker ( sendHTTPCall , {
290+ ...options . circuitBreaker ,
291+ timeout : false ,
292+ autoRenewAbortController : true ,
293+ } ) ;
294+
295+ realBreaker . on ( 'open' , ( ) =>
296+ breakerLogger . error ( 'circuit opened - backend seems unreachable.' ) ,
297+ ) ;
298+ realBreaker . on ( 'halfOpen' , ( ) =>
299+ breakerLogger . info ( 'circuit half open - testing backend connectivity' ) ,
300+ ) ;
301+ realBreaker . on ( 'close' , ( ) => breakerLogger . info ( 'circuit closed - backend recovered ' ) ) ;
302+
303+ // @ts -expect-error missing definition in typedefs for `opposum`
304+ breaker = realBreaker ;
305+ } ,
306+ ( ) => {
307+ breakerLogger . info ( 'circuit breaker not supported on platform' ) ;
308+ breaker = noopBreaker ( ) ;
309+ } ,
310+ ) ;
311+ } else {
312+ breaker = noopBreaker ( ) ;
313+ }
314+
315+ async function sendFromBreaker ( ...args : Parameters < typeof breaker . fire > ) {
316+ if ( ! breaker ) {
317+ await loadCircuitBreakerPromise ;
318+ }
319+
320+ try {
321+ return await breaker . fire ( ...args ) ;
322+ } catch ( err : unknown ) {
323+ if ( err instanceof Error && 'code' in err && err . code === 'EOPENBREAKER' ) {
324+ breakerLogger . info ( 'circuit open - sending report skipped' ) ;
325+ return null ;
326+ }
327+
328+ throw err ;
329+ }
330+ }
331+
218332 return {
219333 capture,
220334 sendImmediately,
0 commit comments