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
- Quick Start
- API Reference
- Configuration
- Event Tracking
- Performance Monitoring
- Sampling
- Integration with LogRocket
- Best Practices
- Examples
- Performance Architecture
- Troubleshooting
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 loggingsamplingRatios?(Record<string, number>): Deprecated - UsetimingEventSamplingRatiosinsteadtimingEventSamplingRatios?(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).
| Destination | Payload | Guards |
|---|---|---|
GTM (gtmDataLayer) | envName, appId, instanceId, userId, ...appParameters, productId, productPath, productUrl, productPageTitle, version, appPath, appUrl, appPageTitle, event, ...callerFields | None |
LogRocket (track) | Caller-supplied fields only (everything except event) — no envName, appId, instanceId, userId, product details, or appParameters | Throttle: 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.warnwheneventExhas 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
lrTrackGuardedwhen you need to send events to LogRocket from code that does not have access to anAnalyticsinstance (e.g. SDK utilities, auth flows, page-level hooks). For business events that should also reach GTM, useAnalytics.sendBAEventinstead.
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:
- 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. - 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.
| Property | Source | Example |
|---|---|---|
message | Hard-coded 'timing' | "timing" |
event | PerformanceMeasure.name | "api-call" |
duration | PerformanceMeasure.duration (stringified ms) | "234.5" |
startTime | performance.timeOrigin + entry.startTime (ISO 8601) | "2026-03-30T14:22:01.123Z" |
detail | PerformanceMeasure.detail object | { startAppId, startAppUrl, endAppId, endAppUrl } |
GTM (gtmDataLayer.push)
Sampling: Head sampling (default 25%). No additional guards.
| Property | Source | Example |
|---|---|---|
event | "timing:<name>" | "timing:api-call" |
envName | sessionStorage.getItem('envName') | "production" |
appId | window.emui.appId | "loan-app" |
instanceId | sessionStorage.getItem('instanceId') | "inst-123" |
userId | sessionStorage.getItem('userId') | "user-456" |
...appParameters | Any fields set via updateBAEventParameters() | varies |
productId | window.parent.emui.appId | "host-app" |
productPath | window.parent.location.pathname | "/host" |
productUrl | window.parent.location.href | "https://host.com/host" |
productPageTitle | window.parent.document.title | "Host App" |
version | window.parent.emui.version | "2.5.0" |
appPath | window.location.pathname | "/loans" |
appUrl | window.location.href | "https://host.com/loans" |
appPageTitle | document.title | "Loan App" |
duration | PerformanceMeasure.duration (stringified ms) | "234.5" |
startTime | performance.timeOrigin + entry.startTime (ISO 8601) | "2026-03-30T14:22:01.123Z" |
startAppId | detail.startAppId (from startTiming options) | "loan-app" |
startAppUrl | detail.startAppUrl (from startTiming options) | "https://host.com/loans" |
endAppId | detail.endAppId (from endTiming options) | "loan-app" |
endAppUrl | detail.endAppUrl (from endTiming options) | "https://host.com/loans" |
Note:
startAppId/startAppUrl/endAppId/endAppUrlcome fromPerformanceMeasure.detailand are only present for measures created viastartTiming/endTiming. Third-party or customperformance.measure()calls will have whateverdetailfields 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.
| Property | Source | Example |
|---|---|---|
event | Caller-supplied event field | "user-login" |
envName | sessionStorage.getItem('envName') | "production" |
appId | window.emui.appId | "loan-app" |
instanceId | sessionStorage.getItem('instanceId') | "inst-123" |
userId | sessionStorage.getItem('userId') | "user-456" |
...appParameters | Any fields set via updateBAEventParameters() | varies |
productId | window.parent.emui.appId | "host-app" |
productPath | window.parent.location.pathname | "/host" |
productUrl | window.parent.location.href | "https://host.com/host" |
productPageTitle | window.parent.document.title | "Host App" |
version | window.parent.emui.version | "2.5.0" |
appPath | window.location.pathname | "/loans" |
appUrl | window.location.href | "https://host.com/loans" |
appPageTitle | document.title | "Loan App" |
...rest | All other caller-supplied fields | varies |
LogRocket (lrTrackGuarded)
Sampling: None (100%). Additional guards: throttle (max 3 identical event+payload per second), property-count warning if >2 props.
| Property | Source | Example |
|---|---|---|
| Track event name | Caller-supplied event field | "user-login" |
...rest | All 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
sendBAEventare 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 matchingGTM-*orG-*) 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:
| Limit | Value |
|---|---|
Max track() calls | 20,000 / session |
| Max total custom properties | 2,000 / session |
| Max property name length | 100 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(),
});