Skip to main content

PUI Analytics Scripting Object - Usage Guide

The @elliemae/pui-analytics-so library provides a comprehensive analytics solution for tracking business events, performance measurements, and user interactions in your applications.

Table of Contents

Installation

npm install @elliemae/pui-analytics-so

or

pnpm add @elliemae/pui-analytics-so

Quick Start

Basic Setup

import { Analytics, updateBAEventParameters } from '@elliemae/pui-analytics-so';
import { Logger } from '@elliemae/pui-diagnostics';

// Initialize logger
const logger = new Logger('MyApp');

// Create analytics instance
const analytics = new Analytics({
logger,
timingEventSamplingRatios: {
'user-interaction': 0.1, // Sample 10% of user interaction events
'page-load': 1.0, // Sample 100% of page load events
'api-call': 0.5, // Sample 50% of API call events
},
});

// Configure global event parameters
updateBAEventParameters({
envName: 'production',
appId: 'my-awesome-app',
instanceId: 'instance-123',
userId: 'user-456',
});

Alternative Setup with Console Logger

import { Analytics } from '@elliemae/pui-analytics-so';

// Use console as logger for development
const analytics = new Analytics({
logger: console,
});

API Reference

Analytics Class

Constructor

new Analytics(params: AnalyticsParams)

Parameters:

  • logger (Logger | typeof console): Logger instance for internal logging
  • samplingRatios? (Record<string, number>): Deprecated - Use timingEventSamplingRatios instead
  • timingEventSamplingRatios? (Record<string, number>): Optional sampling ratios for timing events (0 to 1)

Methods

sendBAEvent(event: BAEvent): Promise\<void\>

Sends a business analytics event to GTM and LogRocket. No sampling is applied — every call reaches both destinations (LogRocket is additionally subject to its sliding-window throttle).

DestinationPayloadGuards
GTM (gtmDataLayer)envName, appId, instanceId, userId, ...appParameters, productId, productPath, productUrl, productPageTitle, version, appPath, appUrl, appPageTitle, event, ...callerFieldsNone
LogRocket (track)Caller-supplied fields only (everything except event) — no envName, appId, instanceId, userId, product details, or appParametersThrottle: max 3 identical event+payload per second; property-count warning if >2 props
await analytics.sendBAEvent({
event: 'user-login',
method: 'oauth',
});
// GTM receives: { event, method, envName, appId, instanceId, userId, productId, productPath,
// productUrl, productPageTitle, version, appPath, appUrl, appPageTitle, ...appParameters }
// LR receives: { method } ← caller fields only, no ambient context
startTiming(name: string, options: TimingOptions): Promise\<PerfMark\>

Starts a performance timing measurement.

const measurement = await analytics.startTiming('api-call', {
appId: 'my-app',
appUrl: 'https://my-app.com/api',
});
endTiming(start: string | PerfMark, options: TimingOptions): Promise\<void\>

Ends a performance timing measurement.

// Using the measurement object
await analytics.endTiming(measurement, {
appId: 'my-app',
appUrl: 'https://my-app.com/api',
});

// Using the measurement name
await analytics.endTiming('api-call', {
appId: 'my-app',
appUrl: 'https://my-app.com/api',
});
setTimingEventSamplingRatio(ratios: Record\<string, number\>): void

Sets sampling ratios for multiple timing events.

analytics.setTimingEventSamplingRatio({
'api-call': 0.1,
'user-interaction': 0.5,
'page-load': 0.8,
});
getTimingEventSamplingRatio(names: string[]): Record\<string, number | undefined\>

Gets sampling ratios for specified timing events.

const ratios = analytics.getTimingEventSamplingRatio([
'api-call',
'user-interaction',
]);
console.log(ratios); // { 'api-call': 0.1, 'user-interaction': 0.5 }
deleteTimingEventSamplingRatio(names: string[]): void

Removes sampling configuration for timing events. Events will fall back to the default sampling ratio (0.25).

analytics.deleteTimingEventSamplingRatio(['api-call', 'user-interaction']);
getAllTimingEventSamplingRatios(): Record\<string, number\>

Gets all configured timing event sampling ratios.

const allRatios = analytics.getAllTimingEventSamplingRatios();
console.log(allRatios); // { 'api-call': 0.1, 'user-interaction': 0.5 }

Utility Functions

updateBAEventParameters(params: Record\<string, any\>): void

Updates global event parameters that will be included in all events.

import { updateBAEventParameters } from '@elliemae/pui-analytics-so';

updateBAEventParameters({
envName: 'staging',
appId: 'updated-app-id',
customProperty: 'custom-value',
});

getBAEventParameters(): BAEventParameters

Retrieves current global event parameters.

import { getBAEventParameters } from '@elliemae/pui-analytics-so';

const currentParams = getBAEventParameters();
console.log(currentParams); // { envName: 'staging', appId: 'updated-app-id', ... }

lrTrackGuarded(event: string, eventEx: Record\<string, unknown\>): void

Throttled wrapper around logRocket.track() that can be imported directly by consumers who need to send custom events to LogRocket without going through the Analytics class.

This is the same function used internally by sendBAEvent and the timing pipeline. It provides:

  • Sliding-window throttle: max 3 identical event+payload calls per second; excess calls are silently dropped
  • Property-count warning: emits console.warn when eventEx has more than 2 properties
import { lrTrackGuarded } from '@elliemae/pui-analytics-so';

// Track a custom event — only caller-supplied fields, no ambient context
lrTrackGuarded('page-view', { pageTitle: 'Dashboard' });

// High-frequency calls are automatically throttled
for (let i = 0; i < 100; i++) {
lrTrackGuarded('scroll', { position: i }); // only first 3 per second reach LogRocket
}

When to use: Import lrTrackGuarded when you need to send events to LogRocket from code that does not have access to an Analytics instance (e.g. SDK utilities, auth flows, page-level hooks). For business events that should also reach GTM, use Analytics.sendBAEvent instead.

invalidateBasicInfoCache(): void

Invalidates the cached sessionStorage values (envName, instanceId, userId) and window.emui.appId. The library caches these on first read to avoid repeated synchronous sessionStorage I/O. Call this function after updating sessionStorage values mid-session so subsequent events pick up the new values.

import { invalidateBasicInfoCache } from '@elliemae/pui-analytics-so';

sessionStorage.setItem('userId', 'new-user-789');
invalidateBasicInfoCache();

Configuration

Environment Setup

The library automatically detects environment information, but you can override defaults:

updateBAEventParameters({
envName: process.env.NODE_ENV || 'development',
appId: process.env.APP_ID || 'default-app',
instanceId: generateInstanceId(),
userId: getCurrentUserId(),
});

Sampling Configuration

Configure sampling ratios to control timing event volume:

// Option 1: Configure during initialization
const analytics = new Analytics({
logger,
timingEventSamplingRatios: {
'high-frequency-event': 0.01, // 1% sampling
'critical-event': 1.0, // 100% sampling
'debug-event': 0.1, // 10% sampling
},
});

// Option 2: Configure dynamically at runtime
analytics.setTimingEventSamplingRatio({
'api-call': 0.5,
'user-interaction': 0.25,
});

// Check current sampling ratios
const ratios = analytics.getTimingEventSamplingRatio([
'api-call',
'user-interaction',
]);
console.log(ratios); // { 'api-call': 0.5, 'user-interaction': 0.25 }

// Remove specific sampling configurations (fall back to default 0.25)
analytics.deleteTimingEventSamplingRatio(['debug-event']);

// Get all configured ratios
const allRatios = analytics.getAllTimingEventSamplingRatios();

Event Tracking

Basic Event Tracking

// Simple event
await analytics.sendBAEvent({
event: 'button-click',
buttonId: 'submit-form',
page: '/checkout',
});

// Event with custom data
await analytics.sendBAEvent({
event: 'form-submission',
formType: 'contact',
fields: ['name', 'email', 'message'],
validationErrors: [],
submissionTime: Date.now(),
});

User Journey Tracking

// Page view
await analytics.sendBAEvent({
event: 'page-view',
page: '/dashboard',
previousPage: '/login',
sessionId: getSessionId(),
});

// User action
await analytics.sendBAEvent({
event: 'feature-usage',
feature: 'document-upload',
action: 'drag-drop',
fileType: 'pdf',
fileSize: 1024000,
});

Performance Monitoring

API Call Monitoring

async function fetchUserData(userId: string) {
// Start timing
const timing = await analytics.startTiming('api-user-fetch', {
appId: 'user-service',
appUrl: window.location.href,
});

try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();

// End timing with success
await analytics.endTiming(timing, {
appId: 'user-service',
appUrl: window.location.href,
status: 'success',
responseSize: JSON.stringify(data).length,
});

return data;
} catch (error) {
// End timing with error
await analytics.endTiming(timing, {
appId: 'user-service',
appUrl: window.location.href,
status: 'error',
errorMessage: error.message,
});
throw error;
}
}

Component Load Time

class MyComponent {
async componentDidMount() {
const timing = await analytics.startTiming('component-load', {
appId: 'my-app',
appUrl: window.location.href,
});

// Simulate component initialization
await this.loadData();
await this.setupEventListeners();

await analytics.endTiming(timing, {
appId: 'my-app',
appUrl: window.location.href,
componentName: 'MyComponent',
});
}
}

Long-Running Operations

async function processLargeDataset(data: any[]) {
const timing = await analytics.startTiming('data-processing', {
appId: 'data-processor',
appUrl: window.location.href,
});

try {
const result = await heavyDataProcessing(data);

await analytics.endTiming(timing, {
appId: 'data-processor',
appUrl: window.location.href,
recordCount: data.length,
outputSize: result.length,
});

return result;
} catch (error) {
await analytics.endTiming(timing, {
appId: 'data-processor',
appUrl: window.location.href,
error: error.message,
});
throw error;
}
}

Performance Monitoring — Data Routing

Every performance.measure() entry on the page is captured by a PerformanceObserver. Before reaching any destination, each entry passes through two gates:

  1. Third-party filter — measures with names matching GTM/GA4 ID patterns (GTM-*, G-*) are silently discarded. These are internal browser measures created by Google Tag Manager and should not be forwarded to the analytics pipeline.
  2. Head sampling — the per-event ratio (default 25%) decides whether the entry is recorded. Sampling applies uniformly to both timing destinations (Splunk and GTM).
performance.measure()  ←── captures ALL measures on the page

├── Filter: GTM/GA4 names (GTM-*, G-*) → discard

└── shouldSample() [25% HeadSampler by default]

├── Splunk (logger.info)
│ payload: { message, event, duration, startTime, detail }

├── GTM (gtmDataLayer.push)
│ payload: BA params + product details + { duration, startTime, ...detail }
│ event: "timing:<measurementName>"

└── LogRocket (lrTrackGuarded)
event: "timing:<measurementName>"
payload: { duration }

Processing is deferred via queueMicrotask so the caller's performance.measure() returns immediately without blocking on logging, GTM pushes, or LogRocket calls.

What each destination receives for timing events

Splunk (logger.info)

Sampling: Head sampling (default 25%). No additional guards.

PropertySourceExample
messageHard-coded 'timing'"timing"
eventPerformanceMeasure.name"api-call"
durationPerformanceMeasure.duration (stringified ms)"234.5"
startTimeperformance.timeOrigin + entry.startTime (ISO 8601)"2026-03-30T14:22:01.123Z"
detailPerformanceMeasure.detail object{ startAppId, startAppUrl, endAppId, endAppUrl }

GTM (gtmDataLayer.push)

Sampling: Head sampling (default 25%). No additional guards.

PropertySourceExample
event"timing:<name>""timing:api-call"
envNamesessionStorage.getItem('envName')"production"
appIdwindow.emui.appId"loan-app"
instanceIdsessionStorage.getItem('instanceId')"inst-123"
userIdsessionStorage.getItem('userId')"user-456"
...appParametersAny fields set via updateBAEventParameters()varies
productIdwindow.parent.emui.appId"host-app"
productPathwindow.parent.location.pathname"/host"
productUrlwindow.parent.location.href"https://host.com/host"
productPageTitlewindow.parent.document.title"Host App"
versionwindow.parent.emui.version"2.5.0"
appPathwindow.location.pathname"/loans"
appUrlwindow.location.href"https://host.com/loans"
appPageTitledocument.title"Loan App"
durationPerformanceMeasure.duration (stringified ms)"234.5"
startTimeperformance.timeOrigin + entry.startTime (ISO 8601)"2026-03-30T14:22:01.123Z"
startAppIddetail.startAppId (from startTiming options)"loan-app"
startAppUrldetail.startAppUrl (from startTiming options)"https://host.com/loans"
endAppIddetail.endAppId (from endTiming options)"loan-app"
endAppUrldetail.endAppUrl (from endTiming options)"https://host.com/loans"

Note: startAppId/startAppUrl/endAppId/endAppUrl come from PerformanceMeasure.detail and are only present for measures created via startTiming/endTiming. Third-party or custom performance.measure() calls will have whatever detail fields they set.

LogRocket

Performance timing events are not sent to LogRocket. This reduces noise in LogRocket sessions and conserves the 2,000 total-property session budget for higher-value BA events.


What each destination receives for BA events (sendBAEvent)

GTM (gtmDataLayer.push)

Sampling: None (100%). No additional guards.

PropertySourceExample
eventCaller-supplied event field"user-login"
envNamesessionStorage.getItem('envName')"production"
appIdwindow.emui.appId"loan-app"
instanceIdsessionStorage.getItem('instanceId')"inst-123"
userIdsessionStorage.getItem('userId')"user-456"
...appParametersAny fields set via updateBAEventParameters()varies
productIdwindow.parent.emui.appId"host-app"
productPathwindow.parent.location.pathname"/host"
productUrlwindow.parent.location.href"https://host.com/host"
productPageTitlewindow.parent.document.title"Host App"
versionwindow.parent.emui.version"2.5.0"
appPathwindow.location.pathname"/loans"
appUrlwindow.location.href"https://host.com/loans"
appPageTitledocument.title"Loan App"
...restAll other caller-supplied fieldsvaries

LogRocket (lrTrackGuarded)

Sampling: None (100%). Additional guards: throttle (max 3 identical event+payload per second), property-count warning if >2 props.

PropertySourceExample
Track event nameCaller-supplied event field"user-login"
...restAll caller-supplied fields except event{ method: "oauth" }

LogRocket receives only the caller-supplied fields — no BA params (envName, appId, instanceId, userId), no product details (productId, productPath, etc.), and no appParameters are injected.

Sampling

Sampling controls how many timing events reach both timing destinations (Splunk and GTM). Unsampled entries are dropped before any processing occurs, meaning they do not generate logger calls or GTM pushes.

Default Sampling Behavior

By default, all timing events are sampled at 25% (0.25 ratio) using a deterministic HeadSampler: every 1st of 4 entries passes, the other 3 are dropped. This is consistent across runs, unlike random sampling.

Configure Sampling

// Configure sampling ratios for specific timing events
const analytics = new Analytics({
logger,
timingEventSamplingRatios: {
'api-call': 0.1, // 1-in-10 events recorded
'page-load': 1.0, // all events recorded
'user-interaction': 0.5, // 1-in-2 events recorded
'component-render': 0.01, // 1-in-100 events recorded
},
});

Dynamic Sampling Management

// Update sampling ratios at runtime
analytics.setTimingEventSamplingRatio({
'api-call': 0.2, // increase to 20%
'new-event': 0.5, // add new event
});

// Check current sampling configuration
const currentRatios = analytics.getTimingEventSamplingRatio([
'api-call',
'page-load',
]);
console.log(currentRatios); // { 'api-call': 0.2, 'page-load': 1.0 }

// Remove specific configurations (reverts to default 0.25)
analytics.deleteTimingEventSamplingRatio(['component-render']);

// View all configured ratios
const allRatios = analytics.getAllTimingEventSamplingRatios();

Important Notes

  • Sampling applies uniformly to both timing destinations (Splunk and GTM) — there is no per-destination sampling. Timing events are not sent to LogRocket
  • Non-timing events sent via sendBAEvent are not sampled — every call reaches both GTM and LogRocket (100%), subject only to LogRocket's throttle
  • Timing events without a configured ratio use the default 0.25 (25%) rate
  • Ratios must be between 0 (never sample) and 1 (always sample)
  • Third-party performance.measure() entries from GTM/GA4 (names matching GTM-* or G-*) are automatically filtered out before sampling

Integration with LogRocket

The library automatically integrates with LogRocket when window.emui.logRocket is set.

Setup LogRocket

window.emui = window.emui || {};
window.emui.logRocket = LogRocket; // Your LogRocket instance

What LogRocket receives

LogRocket receives only caller-supplied fields — no ambient context (getBAEventParameters, product details) is injected. This keeps LR's session-level budgets from being exhausted by high-cardinality context repeated on every event.

await analytics.sendBAEvent({
event: 'document-upload',
fileType: 'pdf', // ← sent to LR
fileSize: 102400, // ← sent to LR
// envName, appId, etc. are NOT sent to LR
});
// LR.track('document-upload', { fileType: 'pdf', fileSize: 102400 })

LogRocket session limits and guards

LogRocket imposes per-session limits that the SDK guards against:

LimitValue
Max track() calls20,000 / session
Max total custom properties2,000 / session
Max property name length100 characters

The lrTrackGuarded function is exported from the package so consumers can import it directly for custom LogRocket tracking without duplicating throttle logic (see Utility Functions).

The wrapper enforces:

Throttling — identical event+payload combinations are capped at 3 calls per second. Subsequent calls within the same window are silently dropped. This prevents hot-path events (e.g. scroll handlers) from consuming the call budget.

Property name truncation — property names longer than 100 characters are automatically truncated to prevent rejection.

Property-count warning — a console.warn is emitted when a caller passes more than 2 properties to a single LR event. This is a signal to reconsider the payload shape, not a hard block.

// Triggers a console.warn: LogRocket payload has 3 custom properties...
await analytics.sendBAEvent({
event: 'complex-event',
prop1: 'a',
prop2: 'b',
prop3: 'c',
});

Timing events and LogRocket

Performance timing events are not sent to LogRocket. This reduces noise in LogRocket sessions and conserves the 2,000 total-property session budget for higher-value BA events. Timing data is captured by Splunk (full payload) and GTM (enriched payload).

Best Practices

1. Event Naming

Use consistent, descriptive event names:

// Good
await analytics.sendBAEvent({ event: 'form-submission-success' });
await analytics.sendBAEvent({ event: 'api-call-failure' });
await analytics.sendBAEvent({ event: 'user-navigation' });

// Avoid
await analytics.sendBAEvent({ event: 'event1' });
await analytics.sendBAEvent({ event: 'stuff-happened' });

2. Context-Rich Events

Include relevant context in events:

await analytics.sendBAEvent({
event: 'document-upload',
documentType: 'loan-application',
fileSize: file.size,
fileType: file.type,
uploadMethod: 'drag-drop',
page: window.location.pathname,
timestamp: new Date().toISOString(),
});

3. Error Handling

Always handle analytics errors gracefully:

try {
await analytics.sendBAEvent({ event: 'user-action' });
} catch (error) {
// Don't let analytics failures break your app
console.warn('Analytics event failed:', error);
}

4. Performance Considerations

Use appropriate sampling for high-frequency timing events:

// High-frequency timing events should use lower sampling rates
const analytics = new Analytics({
logger,
timingEventSamplingRatios: {
'scroll-tracking': 0.01, // 1% sampling
'resize-tracking': 0.01,
'mouse-move-tracking': 0.001, // 0.1% sampling
},
});

The library is designed to minimize impact on the host application:

  • Deferred processing — PerformanceObserver entries are processed via queueMicrotask, so performance.measure() returns immediately.
  • Cached lookupssessionStorage reads and the parent window reference are cached to avoid repeated synchronous I/O. Call invalidateBasicInfoCache() if session values change mid-session.
  • Shared promise — All synchronous methods that return promises reuse a single Promise.resolve() instance to reduce GC pressure.
  • Amortized eviction — The LogRocket throttle map only runs cleanup when it exceeds 50 entries.

5. Keep LogRocket Payloads Lean

LogRocket has a budget of 2,000 total custom properties per session. To stay within it:

  • Send at most 2 key properties per event to LogRocket. The SDK logs a warning if you exceed this.
  • Use GTM/Splunk for rich, multi-property event data.
  • Prefer encoding discriminating information in the event name rather than a property (e.g. timing:PageLoad instead of { event: 'timing', name: 'PageLoad' }).
  • Avoid sending the same high-frequency event with unique payloads; the throttle (3/sec) protects the call count but not the property count.
// Good — 2 properties, stays within budget
await analytics.sendBAEvent({
event: 'loan-saved',
status: 'success',
loanId: '123',
});

// Avoid — 5 properties on a high-frequency event; will trigger a console.warn
await analytics.sendBAEvent({
event: 'field-change',
field: 'loanAmount',
oldValue: 100000,
newValue: 200000,
page: '/loan/edit',
userId: 'u-42',
});

6. Timing Measurements

Always complete timing measurements:

async function operationWithTiming() {
const timing = await analytics.startTiming('operation', {
appId: 'my-app',
appUrl: window.location.href,
});

try {
await performOperation();
} finally {
// Always end timing, even if operation fails
await analytics.endTiming(timing, {
appId: 'my-app',
appUrl: window.location.href,
});
}
}

Examples

React Component Integration

import React, { useEffect } from 'react';
import { Analytics } from '@elliemae/pui-analytics-so';

const UserDashboard: React.FC = () => {
const analytics = new Analytics({ logger: console });

useEffect(() => {
// Track component mount
analytics.sendBAEvent({
event: 'component-mount',
component: 'UserDashboard',
timestamp: new Date().toISOString(),
});

return () => {
// Track component unmount
analytics.sendBAEvent({
event: 'component-unmount',
component: 'UserDashboard',
timestamp: new Date().toISOString(),
});
};
}, []);

const handleButtonClick = async () => {
await analytics.sendBAEvent({
event: 'button-click',
buttonId: 'save-preferences',
page: 'user-dashboard',
});
};

return (
<div>
<h1>Dashboard</h1>
<button onClick={handleButtonClick}>Save Preferences</button>
</div>
);
};

API Service Integration

class APIService {
constructor(private analytics: Analytics) {}

async fetchData(endpoint: string): Promise<any> {
const timing = await this.analytics.startTiming('api-call', {
appId: 'api-service',
appUrl: window.location.href,
});

try {
const response = await fetch(endpoint);

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

const data = await response.json();

await this.analytics.endTiming(timing, {
appId: 'api-service',
appUrl: window.location.href,
endpoint,
status: 'success',
responseSize: JSON.stringify(data).length,
});

await this.analytics.sendBAEvent({
event: 'api-success',
endpoint,
method: 'GET',
responseTime: performance.now() - timing.startTime,
});

return data;
} catch (error) {
await this.analytics.endTiming(timing, {
appId: 'api-service',
appUrl: window.location.href,
endpoint,
status: 'error',
errorMessage: error.message,
});

await this.analytics.sendBAEvent({
event: 'api-error',
endpoint,
method: 'GET',
errorMessage: error.message,
errorType: error.name,
});

throw error;
}
}
}

Performance Architecture

The library is designed to be invisible to the host application's critical path:

TechniqueWhat it avoids
queueMicrotask in PerformanceObserverBlocking the caller's performance.measure() return
Cached pageWindow (evaluated once at module load)Repeated try/catch for cross-origin window.parent.document access
Cached getBasicInfo()3x synchronous sessionStorage.getItem calls per event
Single RESOLVED_VOID promiseAllocating a new Promise object per sendBAEvent / startTiming / endTiming call
buildThrottleKey() using string concatenationJSON.stringify overhead for LR throttle key computation
Throttle check before property-count warningObject.keys iteration on already-throttled (dropped) calls
Amortized eviction (threshold = 50)Full map scan on every new-window LR throttle insertion

Invalidating the basic info cache

getBasicInfo() caches the values from sessionStorage (envName, instanceId, userId) and window.emui.appId on first read. If these values change mid-session (e.g. after a re-authentication), call invalidateBasicInfoCache() so subsequent events pick up the new values:

import { invalidateBasicInfoCache } from '@elliemae/pui-analytics-so';

sessionStorage.setItem('userId', 'new-user-789');
invalidateBasicInfoCache();

Troubleshooting

Common Issues

1. Performance Impact

Problem: Analytics causing performance issues.

Solutions:

  • Implement proper sampling ratios for high-frequency timing events
  • The library defers PerformanceObserver processing via queueMicrotask — if you still see impact, lower sampling ratios
  • Non-timing events sent via sendBAEvent are synchronous but lightweight

2. Stale Session Data in Events

Problem: Events contain outdated envName, userId, or instanceId after session values change.

Solution: Call invalidateBasicInfoCache() after updating sessionStorage values so the cached basic info is refreshed on the next event.

3. Third-Party Timing Events (GTM/GA4)

Problem: Seeing unexpected timing event names like GTM-G-VLQD0B4N or G-XXXXXXX in logs.

Explanation: Google Tag Manager and GA4 create internal performance.measure() calls named after their container/measurement IDs. Since the SDK's PerformanceObserver listens to all measure entries on the page, these were previously captured and forwarded.

Solution: The SDK now automatically filters out performance measures whose names match the pattern GTM-* or G-* (uppercase alphanumeric). No action is required — these entries are silently discarded before sampling or forwarding.

4. Memory Leaks

Problem: Memory usage increasing over time.

Solutions:

  • Clear timing marks after use
  • Avoid storing large objects in event data
  • Implement proper cleanup in components

For more information, check the GitHub repository.