Skip to content

Commit c603273

Browse files
authored
Merge pull request #74 from ninetailed-inc/refactor/ninetailed-core-plugin
Refactor/ninetailed core plugin
2 parents 0bd2f6f + ecb8a5c commit c603273

7 files changed

Lines changed: 413 additions & 217 deletions

File tree

packages/sdks/javascript/src/lib/Ninetailed.ts

Lines changed: 76 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
OnInitProfileId,
3232
PLUGIN_NAME,
3333
PROFILE_CHANGE,
34+
EventHandlerAnalyticsInstance,
3435
} from './NinetailedCorePlugin';
3536
import {
3637
EventFunctionOptions,
@@ -39,7 +40,6 @@ import {
3940
OnProfileChangeCallback,
4041
ProfileState,
4142
TrackHasSeenComponent,
42-
AnalyticsInstance,
4343
TrackComponentView,
4444
} from './types';
4545
import { PAGE_HIDDEN, HAS_SEEN_STICKY_COMPONENT } from './constants';
@@ -142,7 +142,7 @@ const buildOverrideMiddleware =
142142
};
143143

144144
export class Ninetailed implements NinetailedInstance {
145-
private readonly instance: AnalyticsInstance;
145+
private readonly instance: EventHandlerAnalyticsInstance;
146146
private _profileState: ProfileState;
147147
private isInitialized = false;
148148
private readonly apiClient: NinetailedApiClient;
@@ -252,7 +252,7 @@ export class Ninetailed implements NinetailedInstance {
252252
app: 'ninetailed',
253253
plugins: [...this.plugins, this.ninetailedCorePlugin],
254254
...(storageImpl ? { storage: storageImpl } : {}),
255-
}) as AnalyticsInstance;
255+
}) as EventHandlerAnalyticsInstance;
256256

257257
const detachOnReadyListener = this.instance.on('ready', () => {
258258
this.isInitialized = true;
@@ -306,14 +306,7 @@ export class Ninetailed implements NinetailedInstance {
306306
await this.instance.page(data, this.buildOptions(options));
307307
return this.ninetailedCorePlugin.flush();
308308
} catch (error) {
309-
logger.error(error as Error);
310-
311-
if (error instanceof RangeError) {
312-
throw new Error(
313-
`[Validation Error] "page" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`
314-
);
315-
}
316-
throw error;
309+
return this.handleMethodError(error, 'page');
317310
}
318311
};
319312

@@ -337,14 +330,7 @@ export class Ninetailed implements NinetailedInstance {
337330
);
338331
return this.ninetailedCorePlugin.flush();
339332
} catch (error) {
340-
logger.error(error as Error);
341-
342-
if (error instanceof RangeError) {
343-
throw new Error(
344-
`[Validation Error] "track" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`
345-
);
346-
}
347-
throw error;
333+
this.handleMethodError(error, 'track');
348334
}
349335
};
350336

@@ -369,14 +355,7 @@ export class Ninetailed implements NinetailedInstance {
369355
);
370356
return this.ninetailedCorePlugin.flush();
371357
} catch (error) {
372-
logger.error(error as Error);
373-
374-
if (error instanceof RangeError) {
375-
throw new Error(
376-
`[Validation Error] "identify" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`
377-
);
378-
}
379-
throw error;
358+
this.handleMethodError(error, 'identify');
380359
}
381360
};
382361

@@ -414,14 +393,7 @@ export class Ninetailed implements NinetailedInstance {
414393
await Promise.all(promises);
415394
return this.ninetailedCorePlugin.flush();
416395
} catch (error) {
417-
logger.error(error as Error);
418-
419-
if (error instanceof RangeError) {
420-
throw new Error(
421-
`[Validation Error] "batch" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`
422-
);
423-
}
424-
throw error;
396+
this.handleMethodError(error, 'batch');
425397
}
426398
};
427399

@@ -444,14 +416,7 @@ export class Ninetailed implements NinetailedInstance {
444416
});
445417
return this.ninetailedCorePlugin.flush();
446418
} catch (error) {
447-
logger.error(error as Error);
448-
449-
if (error instanceof RangeError) {
450-
throw new Error(
451-
`[Validation Error] "trackStickyComponentView" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`
452-
);
453-
}
454-
throw error;
419+
this.handleMethodError(error, 'trackStickyComponentView');
455420
}
456421
};
457422

@@ -482,51 +447,64 @@ export class Ninetailed implements NinetailedInstance {
482447
payload: ElementSeenPayload,
483448
options?: ObserveOptions
484449
) => {
485-
const { element, ...remaingPayload } = payload;
450+
const { element, ...remainingPayload } = payload;
486451

487452
if (!(element instanceof Element)) {
488-
const isObject = typeof element === 'object' && element !== null;
489-
const constructorName = isObject ? (element as any).constructor.name : '';
490-
const isConstructorNameNotObject =
491-
constructorName && constructorName !== 'Object';
492-
493-
logger.warn(
494-
`ElementSeenObserver.observeElement was called with an invalid element. Expected an Element but got ${typeof element}${
495-
isConstructorNameNotObject ? ` of type ${constructorName}` : ''
496-
}. This call will be ignored.`
497-
);
498-
} else {
499-
const existingPayloads = this.observedElements.get(element);
453+
this.logInvalidElement(element);
454+
return;
455+
}
500456

501-
const delays = this.pluginsWithCustomComponentViewThreshold.map(
502-
(plugin) => plugin.getComponentViewTrackingThreshold()
503-
);
504-
const uniqueDelays = Array.from(new Set([...delays, options?.delay]));
457+
this.storeElementPayload(element, remainingPayload);
458+
this.setupElementObservation(element, options?.delay);
459+
};
505460

506-
if (!existingPayloads) {
507-
this.observedElements.set(element, [remaingPayload]);
508-
} else {
509-
const isPayloadAlreadyObserved = existingPayloads.some((payload) => {
510-
return JSON.stringify(payload) === JSON.stringify(remaingPayload);
511-
});
461+
private logInvalidElement(element: unknown) {
462+
const isObject = typeof element === 'object' && element !== null;
463+
const constructorName = isObject ? (element as any).constructor.name : '';
464+
const isConstructorNameNotObject =
465+
constructorName && constructorName !== 'Object';
512466

513-
if (isPayloadAlreadyObserved) {
514-
return;
515-
}
467+
logger.warn(
468+
`ElementSeenObserver.observeElement was called with an invalid element. Expected an Element but got ${typeof element}${
469+
isConstructorNameNotObject ? ` of type ${constructorName}` : ''
470+
}. This call will be ignored.`
471+
);
472+
}
516473

517-
this.observedElements.set(element, [
518-
...existingPayloads,
519-
remaingPayload,
520-
]);
521-
}
474+
private storeElementPayload(
475+
element: Element,
476+
payload: ObservedElementPayload
477+
) {
478+
const existingPayloads = this.observedElements.get(element) || [];
522479

523-
uniqueDelays.forEach((delay) => {
524-
this.elementSeenObserver.observe(element, {
525-
delay,
526-
});
527-
});
480+
// Check if the payload is already being observed for this element
481+
const isPayloadAlreadyObserved = existingPayloads.some(
482+
(existingPayload) =>
483+
JSON.stringify(existingPayload) === JSON.stringify(payload)
484+
);
485+
486+
if (isPayloadAlreadyObserved) {
487+
return;
528488
}
529-
};
489+
490+
// Store the new or updated payloads
491+
this.observedElements.set(element, [...existingPayloads, payload]);
492+
}
493+
494+
private setupElementObservation(element: Element, delay?: number) {
495+
// Get all relevant delays from plugins and the custom delay
496+
const pluginDelays = this.pluginsWithCustomComponentViewThreshold.map(
497+
(plugin) => plugin.getComponentViewTrackingThreshold()
498+
);
499+
500+
// Ensure we only observe each delay once
501+
const uniqueDelays = Array.from(new Set([...pluginDelays, delay]));
502+
503+
// Set up observation for each delay
504+
uniqueDelays.forEach((delay) => {
505+
this.elementSeenObserver.observe(element, { delay });
506+
});
507+
}
530508

531509
public unobserveElement = (element: Element) => {
532510
this.observedElements.delete(element);
@@ -814,14 +792,28 @@ export class Ninetailed implements NinetailedInstance {
814792
};
815793
};
816794

817-
public onIsInitialized = (onIsInitialized: OnIsInitializedCallback) => {
818-
if (typeof onIsInitialized === 'function') {
795+
// Always throws, never returns
796+
private handleMethodError(error: unknown, method: string): never {
797+
logger.error(error as Error);
798+
799+
if (error instanceof RangeError) {
800+
throw new Error(
801+
`[Validation Error] "${method}" was called with invalid params. Could not validate due to "RangeError: Maximum call stack size exceeded". This can be caused by passing a cyclic data structure as a parameter. Refrain from passing a cyclic data structure or sanitize it beforehand.`
802+
);
803+
}
804+
throw error;
805+
}
806+
807+
public onIsInitialized = (
808+
onIsInitializedCallback: OnIsInitializedCallback
809+
) => {
810+
if (typeof onIsInitializedCallback === 'function') {
819811
if (this.isInitialized) {
820-
onIsInitialized();
812+
onIsInitializedCallback();
821813
} else {
822814
const detachOnReadyListener = this.instance.on('ready', () => {
823815
this.isInitialized = true;
824-
onIsInitialized();
816+
onIsInitializedCallback();
825817
detachOnReadyListener();
826818
});
827819
}

0 commit comments

Comments
 (0)