velora-mobile-sdk
v0.1.5
Published
Analytics SDK for React Native and Expo
Maintainers
Readme
Velora Mobile SDK
Production analytics SDK for React Native and Expo apps with offline queueing, batching, retry/backoff, and lifecycle-aware delivery.
Why teams ship this in production
- Reliable delivery with persisted AsyncStorage queue
- Automatic flushing on interval, app background, and reconnect
- Retry with exponential backoff (failed events stay queued)
- Identity and session lifecycle built in
- Optional auto-tracking for app lifecycle and route changes
- Safe property sanitization (drops unsupported values, trims long strings)
Installation
npm install velora-mobile-sdkRequired peer dependencies:
npm install react react-native @react-native-async-storage/async-storageOptional peers (recommended):
npm install @react-native-community/netinfo expo-application expo-device expo-linking expo-localizationQuick Start (production baseline)
Initialize once at app bootstrap (for example in your root component setup):
import { initAnalytics } from "velora-mobile-sdk";
await initAnalytics({
apiKey: process.env.EXPO_PUBLIC_VELORA_API_KEY ?? "",
baseUrl: "https://api.example.com", // optional, defaults to https://api.veloraapp.io
debug: __DEV__,
autoTrackLifecycle: true,
autoTrackScreens: true,
normalizeScreenNames: true,
autoTrackDeepLinks: true,
});Track events and screens:
import { analytics } from "velora-mobile-sdk";
await analytics.track("Offer Viewed", {
location: "OfferScreen",
properties: { offerId: "offer_123", placement: "hero" },
});
await analytics.screen("OfferScreen", { offerId: "offer_123" });Identify after auth, and reset on logout/account switch:
await analytics.identify({ userId: "user_123" });
await analytics.reset();Configuration
type AnalyticsConfig = {
apiKey: string;
baseUrl?: string; // default: "https://api.veloraapp.io"
batchSize?: number; // default: 20
flushIntervalMs?: number; // default: 10000
sessionTimeoutMs?: number; // default: 30 minutes
debug?: boolean; // default: false
autoTrackLifecycle?: boolean; // default: true
autoTrackScreens?: boolean; // default: true (effective behavior)
normalizeScreenNames?: boolean; // default: true (effective behavior)
autoTrackDeepLinks?: boolean; // default: true
};Public API
import {
analytics,
initAnalytics,
normalizeLocation,
useExpoRouterTracking,
TrackedPressable,
TrackedTouchableOpacity,
createScrollTracker,
} from "velora-mobile-sdk";Core methods:
analytics.track(name, { properties?, location? })analytics.screen(name, properties?)analytics.identify({ userId })analytics.flush()analytics.reset()analytics.updateLocation(location)analytics.getLocation()analytics.shutdown()
Navigation Tracking
Expo Router (RootLayout pattern)
Use this when your app is powered by Expo Router and routing state lives in the root layout:
import { Slot, usePathname } from "expo-router";
import React, { useEffect } from "react";
import { initAnalytics, useExpoRouterTracking } from "velora-mobile-sdk";
export default function RootLayout() {
const route = usePathname();
useEffect(() => {
void initAnalytics({
apiKey: process.env.EXPO_PUBLIC_VELORA_API_KEY ?? "",
autoTrackLifecycle: true,
autoTrackScreens: true,
autoTrackDeepLinks: true,
});
}, []);
// Tracks route changes from Expo Router
useExpoRouterTracking(route);
}Notes:
- Initialize analytics once at app bootstrap.
- Pass the current pathname from
usePathname()intouseExpoRouterTracking(pathname). - If you use
useExpoRouterTracking, do not also callanalytics.updateLocation(...)for the same route changes.
React Navigation bridge
import { NavigationContainer, useNavigationContainerRef } from "@react-navigation/native";
import { analytics } from "velora-mobile-sdk";
export default function AppNavigation() {
const navigationRef = useNavigationContainerRef();
return (
<NavigationContainer
ref={navigationRef}
onStateChange={() => {
const route = navigationRef.getCurrentRoute();
void analytics.updateLocation(route?.name ?? null);
}}
>
{/* screens */}
</NavigationContainer>
);
}updateLocation can emit:
Page Viewed(auto-tracking enabled)Page ExitedwithdurationMswhen changing screensPage Exitedwithreason: "app_background"when app goes to background
Deep Link Attribution
Deep-link tracking is enabled by default (autoTrackDeepLinks: true) and uses expo-linking when available.
Supported query params:
- Velora params:
vl_source,vl_medium,vl_campaign,vl_content,vl_term,vl_link_id - UTM fallbacks:
utm_source,utm_medium,utm_campaign,utm_content,utm_term - Alias fallback for link id:
vlinkId
Behavior:
- Parsed attribution is stored as pending (
velora_pending_attr) - On the next new session, pending attribution is promoted to current (
velora_current_attr) Session Startedincludes attribution fields inproperties- Every event carries
trackingLinkIdandcontext.attributionfor the active session
UI Tracking Helpers
<TrackedPressable
eventName="CTA Clicked"
eventProperties={{ placement: "hero" }}
onPress={handlePress}
/><TrackedTouchableOpacity
eventName="Buy Clicked"
eventProperties={{ sku: "premium_monthly" }}
onPress={handleBuy}
/>Scroll Depth Tracking
import { createScrollTracker, analytics } from "velora-mobile-sdk";
const trackScrollDepth = createScrollTracker(analytics, {
screen: "ArticleScreen",
contentHeight: 2400,
});
// call from onScroll handler
trackScrollDepth(scrollY, viewportHeight);Emits Scroll Depth Reached at 25/50/75/100% milestones.
Delivery and retry behavior
- Queue is persisted in AsyncStorage and survives restarts
- Flush triggers: interval, reconnect, app background, and queue-size threshold
- Flush is skipped when offline or when another flush is already in progress
- Batch size defaults to
20 - On failure, events are re-queued with incremented retry count
- Retries use exponential backoff (
1s,2s,4s, ...) - Events are retried up to 5 times, then dropped
Backend contract
flush() sends POST {baseUrl}/v1/events/batch with headers:
Content-Type: application/jsonX-API-Key: <apiKey>X-Request-Id: <generated>X-Idempotency-Key: <generated>
Payload shape:
{
"sentAt": "2026-04-12T00:00:00.000Z",
"sdk": { "name": "velora-mobile-sdk", "version": "0.1.x" },
"events": [
{
"clientEventId": "evt_...",
"name": "Offer Viewed",
"occurredAt": "2026-04-12T00:00:00.000Z",
"userId": "user_123",
"anonymousId": null,
"sessionId": "sess_...",
"trackingLinkId": "vlink_...",
"properties": { "offerId": "offer_123" },
"context": {
"platform": "ios",
"appVersion": "1.0.0",
"os": "iOS 18.0",
"device": "iPhone 15",
"locale": "en-US",
"timezone": "America/New_York",
"location": "OfferScreen",
"sdk": { "name": "velora-mobile-sdk", "version": "0.1.x" },
"attribution": {
"entryType": "deep_link",
"source": "instagram",
"campaign": "spring_launch",
"vlinkId": "vlink_123"
}
}
}
]
}Automatic events
When session starts (at init or after timeout):
Session Started(auto-tracked)
When autoTrackLifecycle: true (default):
Application OpenedApplication BackgroundedApplication Foregrounded
When location tracking is used (updateLocation / useExpoRouterTracking):
Page Viewed(unlessautoTrackScreens: false)Page Exited(only when screen duration is at least300ms)
Data handling
Stored in AsyncStorage:
- Anonymous ID (
velora_sdk_anonymous_id) - User ID (
velora_sdk_user_id) - Session ID + last activity (
velora_sdk_session_id,velora_sdk_last_activity_at) - Event queue (
velora_sdk_event_queue) - Attribution (
velora_pending_attr,velora_current_attr) - Lifecycle metadata (
velora:first_open_at,velora:last_open_at)
Property sanitization:
- Drops
undefinedand function values - Keeps JSON-serializable objects
- Trims string values to 1000 chars
Production checklist
- Initialize exactly once at startup
- Use a real production API key and endpoint
- Keep
debug: falsein production builds - Wire route changes to
updateLocation(oruseExpoRouterTracking) - Call
identifyonly after auth is confirmed - Call
reseton logout/account switch - Ensure backend handles idempotency headers
- Validate optional peers are installed for richer device/network context
- Call
analytics.shutdown()during controlled app teardown/testing flows
License
ISC
