sitepong
v0.3.11
Published
Official SitePong SDK for error tracking and monitoring
Maintainers
Readme
Installation
npm install sitepongQuick Start
import sitepong from 'sitepong';
sitepong.init({
apiKey: 'your-api-key',
environment: 'production',
release: '1.0.0',
});
// Errors are automatically captured!
// Or capture manually:
sitepong.captureError(new Error('Something went wrong'));Features
Error Tracking
Automatic error capture for uncaught exceptions and unhandled promise rejections. Works in both browser and Node.js environments.
// Automatic capture - just initialize!
sitepong.init({ apiKey: 'your-api-key' });
// Manual capture with context
try {
riskyOperation();
} catch (error) {
sitepong.captureError(error, {
tags: { component: 'checkout' },
extra: { orderId: '12345' },
});
}
// Capture messages
sitepong.captureMessage('User signed up', 'info', {
user: { id: 'user-123' },
});Session Replay
Record and replay user sessions to see exactly what happened before an error. Privacy-focused with automatic input masking.
sitepong.init({
apiKey: 'your-api-key',
replay: {
enabled: true,
maskInputs: true, // Mask sensitive inputs
blockSelector: '.private', // Block specific elements
sampleRate: 0.5, // Record 50% of sessions
recordNetwork: true, // Capture XHR/fetch requests
recordConsole: true, // Capture console logs
},
});
// Manual control
sitepong.startReplay();
sitepong.stopReplay();
const sessionId = sitepong.getReplaySessionId();Feature Flags
Ship features with confidence using real-time feature flags and A/B testing.
sitepong.init({
apiKey: 'your-api-key',
enableFlags: true,
});
// Wait for flags to load
await sitepong.waitForFlags();
// Boolean flags
if (sitepong.getFlag('new-checkout', false)) {
showNewCheckout();
}
// Multivariate flags / A/B tests
const variant = sitepong.getVariant('button-color', 'blue');
const payload = sitepong.getVariantPayload('pricing-test');
// Get all flags
const allFlags = sitepong.getAllFlags();Product Analytics
Understand user behavior with event tracking and autocapture.
sitepong.init({
apiKey: 'your-api-key',
analytics: {
enabled: true,
autocapturePageviews: true,
autocaptureClicks: true,
autocaptureForms: true,
},
});
// Track custom events
sitepong.track('Purchase Completed', {
product: 'Pro Plan',
revenue: 99,
});
// Identify users
sitepong.identify('user-123', {
name: 'John Doe',
email: '[email protected]',
plan: 'pro',
});
// Group users
sitepong.group('company-456', {
name: 'Acme Inc',
industry: 'Technology',
});
// Manual page views
sitepong.trackPageView('/checkout');Device Intelligence & Fraud Detection
Identify devices and detect fraudulent activity with advanced fingerprinting.
sitepong.init({
apiKey: 'your-api-key',
fingerprint: {
enabled: true,
extendedSignals: true,
},
});
// Get stable visitor ID
const { visitorId, confidence } = await sitepong.getVisitorId();
// Get device signals
const signals = await sitepong.getDeviceSignals();
// Fraud detection
const fraud = await sitepong.getFraudCheck();
if (fraud.riskScore > 0.8) {
blockTransaction();
}Performance Monitoring
Track performance with Web Vitals, custom transactions, and distributed tracing.
sitepong.init({
apiKey: 'your-api-key',
performance: {
enabled: true,
webVitals: true,
navigationTiming: true,
resourceTiming: true,
sampleRate: 1.0,
},
});
// Get Web Vitals
const vitals = sitepong.getWebVitals();
// { LCP: 1200, FID: 50, CLS: 0.1, FCP: 800, TTFB: 200 }
// Custom transactions
const txId = sitepong.startTransaction('checkout-flow');
const spanId = sitepong.startSpan(txId, 'validate-cart');
// ... do work
sitepong.endSpan(txId, spanId);
sitepong.endTransaction(txId);
// Distributed tracing
import { createTraceContext, propagateTrace } from 'sitepong';
const trace = createTraceContext();
fetch('/api/order', {
headers: propagateTrace(trace),
});Cron Monitoring
Monitor scheduled jobs and get alerted when they fail or miss a schedule.
sitepong.init({
apiKey: 'your-api-key',
crons: { enabled: true },
});
// Simple check-in
await sitepong.cronCheckin('daily-backup');
// Start/end pattern
const cron = sitepong.cronStart('nightly-sync');
try {
await performSync();
await cron.ok();
} catch (error) {
await cron.error();
}
// Wrap async functions
await sitepong.cronWrap('hourly-cleanup', async () => {
await cleanupOldRecords();
});Custom Metrics
Track business and application metrics with counters, gauges, and histograms.
sitepong.init({
apiKey: 'your-api-key',
metrics: { enabled: true },
});
// Counters
sitepong.metricIncrement('api.requests', 1, { tags: { endpoint: '/users' } });
// Gauges
sitepong.metricGauge('queue.size', 42);
// Histograms
sitepong.metricHistogram('response.size', 1024);
// Distributions
sitepong.metricDistribution('payment.amount', 99.99);
// Timing
await sitepong.metricTime('db.query', async () => {
return await db.query('SELECT * FROM users');
});
// Manual timer
const timer = sitepong.metricStartTimer('operation.duration');
// ... do work
timer.stop();Database Query Tracking
Monitor database performance and detect N+1 query patterns.
sitepong.init({
apiKey: 'your-api-key',
database: {
enabled: true,
slowQueryThreshold: 100, // Log queries over 100ms
redactParams: true, // Redact sensitive parameters
},
});
// Track queries
const users = await sitepong.dbTrack(
'SELECT * FROM users WHERE id = ?',
() => db.query('SELECT * FROM users WHERE id = ?', [userId])
);
// Detect N+1 patterns
const patterns = sitepong.getDbNPlusOnePatterns();
// [{ query: 'SELECT * FROM orders WHERE user_id = ?', count: 50 }]Production Profiling
Profile your production code to identify performance bottlenecks.
sitepong.init({
apiKey: 'your-api-key',
profiling: {
enabled: true,
sampleRate: 0.1, // Profile 10% of operations
maxDuration: 30000, // Max 30s profiles
},
});
// Profile async operations
const result = await sitepong.profile('processOrder', async () => {
return await processOrder(orderId);
});
// Manual spans
const endSpan = sitepong.startProfileSpan('validate');
// ... validation logic
endSpan();React Integration
First-class React support with components and hooks.
import { ... } from 'sitepong/react';Provider & Error Boundary
React Native apps: use
SitePongErrorBoundaryfromsitepong/react-native(see Errors in React Native).
import { SitePongProvider, SitePongErrorBoundary } from 'sitepong/react';
function App() {
return (
<SitePongProvider
apiKey="your-api-key"
config={{
environment: 'production',
analytics: { enabled: true, autocapturePageviews: true },
replay: { enabled: true },
}}
>
<SitePongErrorBoundary fallback={<ErrorPage />}>
<YourApp />
</SitePongErrorBoundary>
</SitePongProvider>
);
}Hooks
import {
// Core
useSitePong,
useCaptureException,
useSetUser,
// Feature Flags
useFeatureFlag,
useExperiment,
// Analytics
useTrack,
useIdentify,
// Fingerprinting
useVisitorId,
useFraudCheck,
// Performance
useWebVitals,
usePerformanceTransaction,
// Replay
useReplay,
} from 'sitepong/react';
function CheckoutButton() {
const track = useTrack();
const showNewDesign = useFeatureFlag('new-checkout-design', false);
const { variant } = useExperiment('button-color');
const handleClick = () => {
track('Checkout Started', { items: cart.length });
// ...
};
return (
<button
onClick={handleClick}
style={{ background: variant === 'red' ? 'red' : 'blue' }}
>
{showNewDesign ? 'Complete Purchase' : 'Checkout'}
</button>
);
}React Native / Expo
First-class React Native support: automatic error capture, navigation tracking,
performance monitoring, release context (OTA update id / app version on every
event), and — with one config flag — Watchtower tap capture + structural
session replay. Everything native ships inside sitepong; there are no
separate @sitepong/* native packages.
Fastest path — let the CLI wire it:
npx @sitepong/cli initIt installs the SDK, adds the config plugin, links (or creates) the project,
and writes EXPO_PUBLIC_SITEPONG_KEY to .env.local.
Manual setup:
npx expo install sitepong @react-native-async-storage/async-storageAdd the single config plugin to app.json:
{
"plugins": ["sitepong"]
}(Opt into Live Activities with ["sitepong", { "liveActivities": true }].)
Then run npx expo prebuild to regenerate native projects. Analytics works in
Expo Go; Watchtower, replay and the persistent device id need a development
build.
import { SitePongRNProvider } from 'sitepong/react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
export default function App() {
const navigationRef = useNavigationContainerRef();
return (
<SitePongRNProvider
apiKey={process.env.EXPO_PUBLIC_SITEPONG_KEY!}
asyncStorage={AsyncStorage}
navigationRef={navigationRef}
config={{
analytics: { enabled: true },
// One flag turns on Watchtower tap capture + structural session replay.
// The API key identifies the project — no projectId / endpoint needed.
watchtower: { enabled: true },
}}
>
<NavigationContainer ref={navigationRef}>
{/* Your screens */}
</NavigationContainer>
</SitePongRNProvider>
);
}apiKey may be omitted when EXPO_PUBLIC_SITEPONG_KEY is set (that is what
npx @sitepong/cli init writes).
Errors in React Native
The provider captures uncaught JS errors (ErrorUtils global handler) and
unhandled promise rejections on its own. For render errors, use the boundary
from sitepong/react-native, not sitepong/react: that one is a separate
bundle whose client the RN provider never initialises, so it reports nowhere.
import { SitePongErrorBoundary, captureError } from 'sitepong/react-native';
<SitePongErrorBoundary
tags={{ screen: 'Checkout' }}
fallback={(error, reset) => <RetryScreen onRetry={reset} />}
>
<Checkout />
</SitePongErrorBoundary>
// Anything you catch yourself:
captureError(err, { tags: { flow: 'payment' } });Without a fallback it renders a plain "Something went wrong / Try again"
screen.
Native crashes (iOS)
Crashes below JS (a fatal signal such as SIGSEGV / SIGABRT, a Swift trap, or an uncaught NSException) are captured with no setup. They need a native rebuild with this SDK version, so an OTA update cannot deliver them.
- The process is dying, so the crash is written to disk and sent on the
next launch as a
fatalerror (taghandler: native-crash). - The event uses the crashed launch's release, update id, build and user, not the next launch's.
- Only the crashed thread's stack is recorded.
- Frames are symbolicated on the server from the build's dSYMs. Builds made by SitePong store them automatically. Frames in Apple's system libraries show as image + offset.
- Issues group by the first frame in your app's code.
To check the pipeline end to end, crash on purpose and relaunch:
import { crashForTesting } from 'sitepong/react-native';
crashForTesting(); // SIGSEGV
crashForTesting(0); // NSException
crashForTesting(6); // SIGABRTIdentify the user
import { identify, resetAnalytics } from 'sitepong/react-native';
identify(user.id, { email: user.email, name: user.name }); // after login / session restore
resetAnalytics(); // on logoutidentify() also sets the user on Watchtower (taps, sessions, replay), including
a user identified before the provider started Watchtower. There is no need to call
setWatchtowerUser as well.
Release context (OTA-aware events)
When expo-updates / expo-application are installed, the provider stamps the
running release on every analytics event, error, Watchtower tap and replay
stream — no code required:
| Key | Source | Notes |
|-----|--------|-------|
| update_id | Updates.updateId | Only for server-delivered updates (never the embedded bundle) |
| update_channel | Updates.channel | |
| runtime_version | Updates.runtimeVersion | |
| app_version | Application.nativeApplicationVersion | Marketing version |
| app_build | Application.nativeBuildVersion | Build number |
| is_embedded_launch | Updates.isEmbeddedLaunch | |
Errors additionally get release = update_id (else app_version) unless you
set release explicitly, so source maps uploaded for that OTA update
symbolicate its stack traces automatically (the sitepong/publish-ota action
uploads them).
Add your own sticky keys the same way:
import { registerSuperProperties } from 'sitepong/react-native';
registerSuperProperties({ plan: 'pro', 'flag.new_checkout': 'on' });React Native Hooks
import {
useScreenTrack,
useAppState,
useRemoteConfig,
useRNPerformance,
} from 'sitepong/react-native';
function MyScreen() {
useScreenTrack('MyScreen');
const appState = useAppState();
const config = useRemoteConfig();
const perf = useRNPerformance('MyScreen');
// perf.markRenderComplete() when screen is ready
}Screen Recording
Captures H.264 video in a rolling buffer. The native module ships inside
sitepong (config plugin "sitepong") — nothing extra to install.
Two modes:
On Error (via Provider)
Recording starts automatically and keeps the last 10 seconds. When an error is captured the buffer is automatically flushed and attached to the error. No video is sent during normal usage.
<SitePongRNProvider
apiKey="your-api-key"
asyncStorage={AsyncStorage}
screenRecording={{
enabled: true,
fps: 1,
quality: 'standard',
// bufferDuration defaults to 10_000 (10s) in on-error mode
}}
>
{/* Your app */}
</SitePongRNProvider>When captureError() is called (or the global error handler fires), the SDK automatically flushes the last 10 seconds of video and attaches it to the error.
Manual
Start and stop recording yourself. Keeps the last 60 seconds by default. Call flushScreenRecording() to upload when you decide.
import {
startScreenRecording,
stopScreenRecording,
flushScreenRecording,
} from 'sitepong/react-native';
// Start — keeps a 60s rolling buffer
startScreenRecording('your-api-key', 'https://ingest.sitepong.com', sessionId, {
fps: 1,
quality: 'standard',
// bufferDuration defaults to 60_000 (60s) in manual mode
});
// Upload the buffer whenever you want
await flushScreenRecording();
// Stop and discard the buffer
stopScreenRecording();Masking sensitive content:
import { SensitiveView } from 'sitepong/react-native';
// Accepts `style` — pass the child's layout (e.g. flex: 1) when wrapping a flex child / WebView.
<SensitiveView>
<Text>SSN: 123-45-6789</Text>
<Text>Card: 4111-1111-1111-1111</Text>
</SensitiveView>Content inside <SensitiveView> is replaced with a black rectangle in recorded video.
Configuration:
| Option | Default (on-error) | Default (manual) | Description |
|---|---|---|---|
| enabled | false | n/a | Enable via provider |
| fps | 1 | 1 | Frames per second (1-10) |
| quality | 'standard' | 'standard' | 'low' / 'standard' / 'high' |
| sampleRate | 1.0 | 1.0 | Fraction of sessions to record (0-1) |
| bufferDuration | 10000 (10s) | 60000 (60s) | Rolling buffer size in ms |
| maxDuration | 3600000 | 3600000 | Max recording duration in ms |
Push Notifications & Live Activities
Native APNs (iOS) and FCM (Android) push notifications, plus iOS Live Activity updates. SitePong sends directly to Apple/Google — no Expo Push service in the path.
Token lifecycle
There are two halves to push: the client SDK registers device tokens with the SitePong ingest server, and the server-side Push API sends notifications and binds tokens to authenticated users.
[App launches]
↓
[Notifications.getDevicePushTokenAsync()] ← OS gives you a native token
↓
[registerDeviceToken(token, opts)] ← SDK POSTs to /api/push/tokens
↓ with X-API-Key: sp_live_xxx
[Token stored anonymously in SitePong]
↓
[User logs in with your backend]
↓
[Your backend → POST /api/push/tokens/associate] ← sp_push_xxx + { token, user_id }
↓
[Your backend → POST /api/push/send] ← sp_push_xxx + target user_ids
↓
[SitePong → APNs / FCM → Device]Why two steps? Publishable SDK keys (
sp_live_xxx) are embedded in your app binary and can be extracted by anyone who decompiles the APK/IPA. If the client controlleduser_id, a leaked SDK key would let an attacker register their own device under your users' IDs and silently receive their push notifications (password resets, OTP codes, order details). User attribution therefore only happens server-to-server with the privilegedsp_push_xxxkey — which stays on your backend.
1. Install permission and get the native token
npx expo install expo-notificationsimport * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
if (!Device.isDevice) return; // simulator can't receive push
const { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') return;
// Returns the raw native token (APNs hex on iOS, FCM token on Android)
const { data: token } = await Notifications.getDevicePushTokenAsync();2. Register the token with SitePong
import { registerDeviceToken } from '@sitepong/sdk/react-native';
import { Platform } from 'react-native';
registerDeviceToken(token, {
platform: Platform.OS as 'ios' | 'android',
environment: __DEV__ ? 'sandbox' : 'production',
});This makes a POST /api/push/tokens request to the ingest server with X-API-Key: <your SDK key>. The body includes the token, token_type (apns/fcm), device_id, platform, app version, device model, and OS version — no user_id. The server upserts by (project_id, token) so re-registration is idempotent.
3. Associate the token with a user (server-side)
Once your own backend has authenticated the user, have it POST to SitePong's associate endpoint with your sp_push_xxx key. Your app should forward the push token to your backend as part of the login flow.
curl -X POST https://api.sitepong.com/api/push/tokens/associate \
-H "Authorization: Bearer sp_push_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "token": "<expo_or_native_token>", "user_id": "user-123" }'On sign-out, POST the same endpoint with "user_id": null to clear the association. Because this call is server-authenticated, an attacker who has scraped the SDK key out of your app cannot impersonate a user.
The older pattern of passing
userIdtoregisterDeviceToken()orpushUserIdtoSitePongRNProviderstill type-checks for backwards compatibility, but the values are silently ignored on both the client and the server. Migrate to the associate endpoint.
4. Send notifications from your backend
Generate a separate Push API key (prefix sp_push_) from the Notifications tab in the SitePong dashboard. This is not the SDK key — it's server-only and has permission to send pushes.
curl -X POST https://api.sitepong.com/api/push/send \
-H "Authorization: Bearer sp_push_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "Order Shipped",
"body": "Your order #1234 is on its way",
"data": { "orderId": "1234" },
"target": { "user_ids": ["user-123"] }
}'Targeting options (provide at least one):
user_ids: string[]— sends to all active devices for these usersdevice_ids: string[]— sends to specific SitePong device IDstokens: string[]— sends directly to raw APNs/FCM tokens
Live Activities (iOS)
SitePong ships a turnkey Live Activity package — a generic SwiftUI Widget Extension that the bundled config plugin generates at prebuild, plus a JavaScript API that drives it from data. No Swift required. Customers compose their Live Activity using SF Symbols, custom progress bars, and styled text fields entirely from JS.
The Live Activity module ships inside sitepong. Opt in through the config
plugin in app.json / app.config.js:
{
"plugins": [["sitepong", { "liveActivities": true }]]
}Then prebuild and rebuild your app (npx expo prebuild --clean). The plugin creates a Widget Extension target named SitePongLiveActivityWidget containing the SitePong template, sets NSSupportsLiveActivities in the main app, and wires the extension into the Xcode project.
Start a Live Activity from JavaScript:
import { startLiveActivity, updateLiveActivity, endLiveActivity } from 'sitepong/react-native';
const { activityId } = await startLiveActivity({
activityType: 'order-tracking',
attributes: { id: '#1234' },
contentState: {
title: 'Order #1234',
subtitle: 'Driver is on the way',
titleStyle: { size: 16, weight: 'bold', color: '#111827' },
subtitleStyle: { size: 14, weight: 'regular', color: '#6b7280' },
leadingIcon: { symbol: 'shippingbox.fill', color: '#3b82f6' },
trailingIcon: { symbol: 'checkmark.seal.fill', color: '#10b981' }, // hidden until you set it
progress: {
style: 'bar-with-icon', // 'bar' | 'bar-with-icon' | 'circular' | 'timer'
value: 0.6,
icon: 'car.fill', // SF Symbol slides along the track
iconColor: '#3b82f6',
trackColor: '#e5e7eb',
fillColor: '#3b82f6',
label: '15 minutes away',
},
},
});Update it locally as state changes:
await updateLiveActivity(activityId, {
title: 'Order #1234',
subtitle: 'Driver is two blocks away',
progress: { style: 'bar-with-icon', value: 0.9, icon: 'car.fill', fillColor: '#3b82f6' },
});Or update it remotely from your backend by calling POST /api/push/live-activity (see endpoint table below). The package automatically forwards the per-activity push token to SitePong on startLiveActivity, so backend updates reach the right device with no extra wiring.
End the activity when the work completes:
await endLiveActivity(activityId, {
finalContentState: {
title: 'Delivered',
leadingIcon: { symbol: 'checkmark.seal.fill', color: '#10b981' },
},
dismissalDate: Date.now() + 60_000, // remove from lock screen after 1 minute
});The Dynamic Island, the app icon, and taps. dynamicIsland sets the compact slots on either side of the island and the minimal one (shown when two apps share it). A slot is an icon, text, or both. { appIcon: true } draws the app's own icon: the config plugin copies it into the extension at prebuild, from liveActivityIcon in the plugin options, else ios.icon, else icon (PNG or JPEG). A tap on the activity always opens the app, and url picks the screen:
await startLiveActivity({
activityType: 'table',
contentState: {
title: 'Table 42',
subtitle: '5 players at the table',
leadingIcon: { appIcon: true },
dynamicIsland: {
compactLeading: { appIcon: true },
compactTrailing: {
icon: { symbol: 'person.2.fill', size: 12, color: '#34C759' },
text: '5',
style: { weight: 'semibold', monospacedDigit: true },
},
minimal: { appIcon: true },
},
url: 'myapp://table/42', // handled like any deep link (Linking / expo-router)
},
});Starting from your backend (iOS 17.2+). Call enableLiveActivityPushToStart('table') once, for example after sign-in. It registers the device's push-to-start token and re-registers it whenever iOS rotates it. POST /api/push/live-activity with event: 'start' and activity_type: 'table' then starts the activity on the device. The started activity reports its own update token, so later update and end pushes reach it. A device has one push-to-start token, so it's registered under the last activity type passed.
Every update is the whole state. ActivityKit replaces the content state on each update (local or push) instead of merging it, so send the full object every time, including dynamicIsland and url.
After a relaunch. A Live Activity outlives the app process. Call getLiveActivities() at startup: it returns the running ones (activityId, activityType, state, pushToken) and re-registers their tokens, so pushes keep reaching them. startLiveActivity and updateLiveActivity also take staleDate (epoch ms) and relevanceScore.
Push capability. A push-updatable activity needs the aps-environment entitlement, which the plugin adds. A build without it, such as an unsigned simulator build, still gets a local-only activity: pushToken comes back empty and only updateLiveActivity reaches it.
Visibility-driven slots. Every visual element in contentState is optional. The SwiftUI template hides slots whose value is omitted, so you control layout density entirely from data. Set trailingIcon only on the final "delivered" update and it appears; omit it everywhere else and it stays hidden.
Available styles. Title and subtitle accept size, weight (ultraLight...black), color (hex), italic, monospacedDigit. Icons accept any SF Symbol name plus color, weight, size, or { appIcon: true } with size and cornerRadius. Progress supports bar, bar-with-icon (the Uber-Eats-style sliding icon), circular, and timer (Apple's auto-updating countdown).
Bringing your own widget. If you need a visually distinctive Live Activity beyond what the template covers, you can skip the bundled template (leave liveActivities off) and write the Widget Extension yourself in Swift, then call the lower-level token registration directly:
import {
registerLiveActivityToken,
registerPushToStartToken,
endLiveActivity as endLiveActivityToken,
} from 'sitepong/react-native';
registerLiveActivityToken('MyActivityAttributes', activity.id, pushToken, { environment: 'production' });
registerPushToStartToken('MyActivityAttributes', pushToStartToken, { environment: 'production' });
endLiveActivityToken('MyActivityAttributes', activity.id);Token invalidation
When APNs or FCM reports a token as invalid (app uninstalled, notifications disabled, token rotated), SitePong marks it inactive on the server side and stops sending. No client action needed. To force a refresh after an OS-level token change, call registerDeviceToken() again with the new token.
Endpoint reference
| Method | Path | Auth | SDK function |
|---|---|---|---|
| POST | /api/push/tokens | X-API-Key: sp_live_* | registerDeviceToken() |
| POST | /api/push/live-activity-tokens | X-API-Key: sp_live_* | registerLiveActivityToken() |
| POST | /api/push/push-to-start-tokens | X-API-Key: sp_live_* | registerPushToStartToken() |
| DELETE | /api/push/live-activity-tokens | X-API-Key: sp_live_* | endLiveActivity() |
| POST | /api/push/tokens/associate | Authorization: Bearer sp_push_* | (server-side only) |
| POST | /api/push/send | Authorization: Bearer sp_push_* | (server-side only) |
| POST | /api/push/live-activity | Authorization: Bearer sp_push_* | (server-side only) |
Default ingest endpoint: https://ingest.sitepong.com. Override via the endpoint option in initRN() or SitePongRNProvider.
Device Intelligence (React Native)
Persistent device identification that survives app reinstalls. The native
module ships inside sitepong (config plugin "sitepong"); nothing extra to
install.
The SDK automatically uses the native device ID:
- iOS: UUID stored in Keychain (persists across uninstall/reinstall, cleared on factory reset)
- Android: SHA-256 of
ANDROID_ID+ SharedPreferences UUID (stable per signing key since Android 8)
import { fetchPersistentDeviceId, fetchNativeDeviceSignals } from 'sitepong/react-native';
// Get persistent device ID
const deviceId = await fetchPersistentDeviceId();
// Get full native signals
const signals = await fetchNativeDeviceSignals();
// { deviceId, platform, osVersion, model, manufacturer, isEmulator, screenScale,
// identifierForVendor (iOS), androidId (Android) }Identity hierarchy (all platforms):
| Level | Scope | Persistence |
|-------|-------|-------------|
| deviceId | Hardware-level | Survives reinstalls (RN only) |
| anonymousId | App-level UUID | Persists in storage |
| sessionId | Session-scoped | 30-min timeout |
| userId | Developer-set | Via identify() |
SQLite Query Tracking
Monitor expo-sqlite query performance with automatic timing and slow query detection.
import { createTrackedDatabase } from 'sitepong/react-native';
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabaseSync('myapp.db');
const trackedDb = createTrackedDatabase(db, {
slowQueryThreshold: 100, // ms — log queries slower than this
trackAll: false, // if true, track every query
});
// Use trackedDb exactly like db — same API
const users = trackedDb.getAllSync('SELECT * FROM users WHERE active = 1');Tracked queries emit $sqlite_query events with { sql, durationMs, rowCount } and add breadcrumbs for debugging.
What's Included
- Error Capture: Automatic
ErrorUtils.setGlobalHandler()+ promise rejection tracking - Screen Recording: H.264 rolling buffer — on-error (10s, auto-attached) or manual (60s)
- Device Intelligence: Persistent device ID via Keychain (iOS) / ANDROID_ID (Android)
- SQLite Tracking: Query timing and slow query detection for expo-sqlite
- Navigation Tracking: React Navigation integration, tracks
$screen_viewevents + breadcrumbs - Network Interception: XHR monkey-patch for request tracking + breadcrumbs
- Performance: Cold start timing, screen render tracking
- App State: Automatic foreground/background/inactive tracking
- AsyncStorage: Persistent storage for session data and remote config cache
LightBeam (guided UI)
sitepong/lightbeam lets your assistant show people where something lives, or do it for them: it moves to the right page, spotlights the control, and in drive mode counts down ("Clicking this in 3s") before clicking or typing. The person can pause, skip, take over or stop at any time.
import { createLightBeam } from 'sitepong/lightbeam';
import { track } from 'sitepong';
const lb = createLightBeam({
navigate: (path) => router.navigate(path),
onDriveConsent: () => askUser('Do it for me?'), // no answer = guide only
track, // lightbeam_run / lightbeam_step into Watchtower
});
const guide = {
id: 'drivers.invite',
steps: [
{ action: 'click', route: '/drivers', target: 'drivers.invite', say: 'Open the invite form' },
{ action: 'type', target: 'drivers.invite.name', value: '{{name}}', say: 'Their name' },
{ action: 'highlight', target: 'drivers.invite.send', say: 'Press Send when it looks right' },
],
};
const run = lb.run(guide, { mode: 'drive', params: { name: 'Ana Diaz' } });
await run.done; // { status: 'completed' | 'cancelled' | 'failed' | 'taken_over', stepIndex }- Tag targets with
data-lightbeam="<id>"; CSS selectors still work as a fallback. Hidden, zero-size or off-screen copies count as missing (optional: trueskips them). - LightBeam only clicks or types into elements tagged
data-lightbeam-drivable(or allowed viadrivable), only afteronDriveConsentsays yes, and never on destructive wording. Anything else is guided: spotlit, waiting for the person. - Dropdowns and menus: the step waits for the pick, and the dimming and strip move out of the popup's way. Tag your own dock or bars
data-lightbeam-uiso they are not treated as outside clicks. - From a Back Office reply: an agent line
<LightBeam guide="drivers.invite" name="Ana"/>arrives indata.widgets;lb.fromWidget(widget, catalogue)starts it. sitepong/copilotis the old name and re-exports everything here.
Remote Config
The SDK can fetch behavior configuration from SitePong servers on init, allowing you to change sampling rates, feature toggles, and transport settings without publishing a new SDK version.
sitepong.init({
apiKey: 'your-api-key',
remoteConfig: { enabled: true },
});
// Check if a remote config feature is enabled
const enabled = sitepong.isRemoteConfigFeatureEnabled('session_replay');
// Get full remote config
const config = sitepong.getRemoteConfig();
// Listen for config changes
sitepong.onRemoteConfigChange((newConfig) => {
console.log('Config updated:', newConfig);
});Remote config is managed from the SDK Config page in the SitePong dashboard, where you can adjust:
- Sampling rates for errors, analytics, replay, and performance
- Feature toggles for any SDK feature
- Autocapture settings (pageviews, clicks, forms, network requests)
- Transport settings (flush interval, batch size, retry attempts)
The SDK caches config locally (with configurable TTL) and refreshes in the background, so your app always starts instantly with the last-known config.
Remote overrides: feature flags, but for values
Any SDK setting your app passes in code can be overridden from the dashboard (Watchtower → Remote config) without a release. An override names the setting by its path and gives the value to use instead:
| Key | Value | Effect |
|---|---|---|
| watchtower.enabled | false | Turns Watchtower off: taps, screenshots and replay (structural capture too). Live. |
| watchtower.structural | false | Keeps taps and screenshots; skips the view-tree replay stream. Live. |
| watchtower.sampleRate | 0.1 | Share of sessions that capture screenshot drill-downs. Next launch. |
| watchtower.ignoreChannels | ["development"] | Release channels that capture nothing. Next launch. |
| screenRecording.enabled | false | The on-error video buffer (React Native). Live. |
| captureErrors, trackNetwork, trackAppState, debug | true/false | Next launch. |
An override can target environments (by API key), platforms (ios, react-native-ios, web…), OTA channels and an app-version range; when several match, the highest priority wins. The app's code stays the default, and deleting an override restores it.
The SDK applies the overrides it cached on the previous launch before it starts anything, so a kill switch holds from a cold start and offline. "Live" settings also change mid-session when the config refreshes (every couple of minutes).
Keys outside the SDK's own settings are your app's values:
import { getRemoteValue } from 'sitepong';
import { useRemoteValue } from 'sitepong/react-native';
const maxPhotos = getRemoteValue('listing.maxPhotos', 8);
const banner = useRemoteValue('home.banner', null); // re-renders when it changesNative iOS apps (Watchtower.start) honour the watchtower.* overrides the same way; read others with WatchtowerRemoteOverrides.value("key", default:).
Framework Integration
Next.js
// app/providers.tsx
'use client';
import { SitePongProvider } from 'sitepong/react';
export function Providers({ children }) {
return (
<SitePongProvider
apiKey={process.env.NEXT_PUBLIC_SITEPONG_KEY}
config={{
environment: process.env.NODE_ENV,
replay: { enabled: true },
}}
>
{children}
</SitePongProvider>
);
}Express
import express from 'express';
import sitepong from 'sitepong';
const app = express();
sitepong.init({
apiKey: process.env.SITEPONG_KEY,
environment: 'production',
});
// Error handling middleware
app.use((err, req, res, next) => {
sitepong.captureError(err, {
extra: {
url: req.url,
method: req.method,
userId: req.user?.id,
},
});
next(err);
});Node.js
import sitepong from 'sitepong';
sitepong.init({
apiKey: process.env.SITEPONG_KEY,
environment: process.env.NODE_ENV,
});
// Uncaught exceptions and unhandled rejections
// are automatically capturedConfiguration Reference
sitepong.init({
// Required
apiKey: 'your-api-key',
// Core
endpoint: 'https://ingest.sitepong.com',
environment: 'production',
release: '1.0.0',
autoCapture: true,
maxBatchSize: 10,
flushInterval: 5000,
debug: false,
// Feature Flags
enableFlags: false,
flagsEndpoint: undefined,
// Analytics
analytics: {
enabled: false,
autocapturePageviews: false,
autocaptureClicks: false,
autocaptureForms: false,
},
// Session Replay
replay: {
enabled: false,
maskInputs: true,
blockSelector: undefined,
maskSelector: undefined,
sampleRate: 1.0,
maxSessionDuration: 3600000,
recordNetwork: false,
recordConsole: false,
},
// Performance
performance: {
enabled: false,
webVitals: true,
navigationTiming: true,
resourceTiming: false,
sampleRate: 1.0,
},
// Fingerprinting
fingerprint: {
enabled: false,
extendedSignals: false,
},
// Cron Monitoring
crons: {
enabled: false,
},
// Custom Metrics
metrics: {
enabled: false,
flushInterval: 10000,
maxBatchSize: 100,
},
// Database Tracking
database: {
enabled: false,
slowQueryThreshold: 100,
redactParams: true,
},
// Profiling
profiling: {
enabled: false,
sampleRate: 0.1,
maxDuration: 30000,
},
});API Reference
Core
| Method | Description |
|--------|-------------|
| init(config) | Initialize the SDK |
| captureError(error, context?) | Capture an error |
| captureMessage(message, level?, context?) | Capture a message |
| setUser(user) | Set user context |
| setTags(tags) | Set tags |
| setContext(context) | Set additional context |
| flush() | Flush the error queue |
Feature Flags
| Method | Description |
|--------|-------------|
| getFlag(key, defaultValue?) | Get boolean flag |
| getVariant(key, defaultValue?) | Get variant key |
| getVariantPayload(key, defaultValue?) | Get variant payload |
| getAllFlags() | Get all flags |
| waitForFlags() | Wait for flags to load |
| areFlagsReady() | Check if flags are loaded |
| refreshFlags() | Refresh flags from server |
Analytics
| Method | Description |
|--------|-------------|
| track(event, properties?) | Track custom event |
| trackPageView(url?, properties?) | Track page view |
| identify(userId, traits?) | Identify user |
| group(groupId, traits?) | Group users |
| resetAnalytics() | Reset analytics state |
Session Replay
| Method | Description |
|--------|-------------|
| startReplay() | Start recording |
| stopReplay() | Stop recording |
| isReplayRecording() | Check if recording |
| getReplaySessionId() | Get session ID |
Performance
| Method | Description |
|--------|-------------|
| startTransaction(name, data?) | Start transaction |
| endTransaction(id, status?) | End transaction |
| startSpan(txId, name, data?) | Start span |
| endSpan(txId, spanId, status?) | End span |
| getWebVitals() | Get Web Vitals |
Fingerprinting
| Method | Description |
|--------|-------------|
| getVisitorId() | Get visitor ID |
| getDeviceSignals() | Get device signals |
| getFraudCheck() | Run fraud check |
Metrics
| Method | Description |
|--------|-------------|
| metricIncrement(name, value?, options?) | Increment counter |
| metricGauge(name, value, options?) | Set gauge |
| metricHistogram(name, value, options?) | Record histogram |
| metricDistribution(name, value, options?) | Record distribution |
| metricTime(name, fn, options?) | Time async function |
| metricStartTimer(name, options?) | Start manual timer |
Cron Monitoring
| Method | Description |
|--------|-------------|
| cronCheckin(slug, options?) | Simple check-in |
| cronStart(slug, environment?) | Start cron job |
| cronWrap(slug, fn, environment?) | Wrap async function |
Database
| Method | Description |
|--------|-------------|
| dbTrack(query, fn, source?) | Track async query |
| dbTrackSync(query, fn, source?) | Track sync query |
| getDbQueryCount() | Get query count |
| getDbNPlusOnePatterns() | Get N+1 patterns |
Remote Config
| Method | Description |
|--------|-------------|
| getRemoteConfig() | Get full remote config |
| isRemoteConfigFeatureEnabled(feature) | Check if remote feature is enabled |
| onRemoteConfigChange(callback) | Listen for config changes (returns unsubscribe fn) |
Profiling
| Method | Description |
|--------|-------------|
| profile(name, fn, metadata?) | Profile async function |
| startProfileSpan(name, metadata?) | Start profile span |
| getProfiles() | Get all profiles |
| getLatestProfile() | Get latest profile |
| flushProfiles() | Flush profiles |
Documentation
For full documentation, visit sitepong.com/docs.
License
MIT
