@renesis_tech/gameanalytics-rn
v0.4.1
Published
React Native wrapper for GameAnalytics Android SDK (design, progression, business, resource, ad, ad-impression/ILRD, error events)
Downloads
816
Maintainers
Readme
@renesis_tech/gameanalytics-rn
React Native wrapper for the GameAnalytics Android SDK. Plug-and-play TurboModule with full TypeScript types — drop it into any React Native app, call initialize, and start tracking design / progression / business events.
Platform: Android only. iOS support is not provided.
Install
# yarn
yarn add @renesis_tech/gameanalytics-rn
# npm
npm install @renesis_tech/gameanalytics-rnReact Native autolinking discovers the module — no MainApplication.kt edits required.
One required host-app change
The GameAnalytics Android SDK isn't on Maven Central — it lives on GameAnalytics' own Maven server. Add the repo to your host app's android/build.gradle so transitive resolution succeeds:
// android/build.gradle (your host app's root build.gradle)
allprojects {
repositories {
maven { url "https://maven.gameanalytics.com/release" }
}
}Without this, gradle will fail with Could not find com.gameanalytics.sdk:gameanalytics-android:+. This is the only manual step.
Requirements
| Tool | Version |
|---|---|
| React Native | 0.74+ (new architecture / TurboModules) |
| Android minSdkVersion | 24 |
| Android compileSdkVersion | 34+ |
The GameAnalytics Android SDK (com.gameanalytics.sdk:gameanalytics-android:+) is pulled in transitively once the repo above is configured.
Quickstart
import { useEffect } from 'react';
import GameAnalytics from '@renesis_tech/gameanalytics-rn';
export default function App() {
useEffect(() => {
// OPTIONAL — tag the build before initialize so it shows up in the
// GameAnalytics dashboard's "Build" filter.
GameAnalytics.configureBuild('1.0.0');
// REQUIRED — initialize exactly once, after the first Activity is
// resumed. A top-level useEffect on mount is the safe spot.
GameAnalytics.initialize('YOUR_GAME_KEY', 'YOUR_SECRET_KEY');
// Optional — turn on info-level logcat while integrating.
GameAnalytics.setEnabledInfoLog(__DEV__);
}, []);
return /* ...your app... */;
}Then anywhere in your app:
GameAnalytics.trackDesignEvent('menu:tap:start');Keeping keys out of source code
The quickstart hardcodes the gameKey/secretKey for clarity. In a real app, keep them out of source so you can use different keys per environment (dev / staging / prod) and so the keys don't sit in your git history.
Security note: the GameAnalytics secret key is intended to live in the client — it signs each event payload locally. Anyone who decompiles your APK can extract it. Env separation is for build hygiene (right key per build), not security. If you need server-side analytics with private secrets, GA's HTTP API is the way to go, not the client SDK.
Three patterns, pick whichever fits your build pipeline:
Option A: react-native-config with .env files (most common)
Install once in your host app:
yarn add react-native-config
cd ios && pod install # not relevant for Android-only, skipCreate .env.development and .env.production at the host app root:
# .env.development
GA_GAME_KEY=062fb3ea732ddbd68a5e85be70815856
GA_SECRET_KEY=95f87e95f7e3b4ec7a8df02df02a17e9ff394dbe
GA_BUILD=1.0.0-dev# .env.production
GA_GAME_KEY=<prod-key>
GA_SECRET_KEY=<prod-secret>
GA_BUILD=1.0.0In your app:
import Config from 'react-native-config';
import GameAnalytics from '@renesis_tech/gameanalytics-rn';
useEffect(() => {
GameAnalytics.configureBuild(Config.GA_BUILD!);
GameAnalytics.initialize(Config.GA_GAME_KEY!, Config.GA_SECRET_KEY!);
}, []);Run a specific env:
ENVFILE=.env.production npx react-native run-androidMake sure .env.* is in .gitignore. Commit a .env.example with placeholder values so other devs know what's expected.
Option B: Android build flavors (build-time only)
If you don't want a JS-side env layer, you can inject the keys at gradle build time via buildConfigField and a tiny Kotlin shim.
In your app's android/app/build.gradle:
android {
flavorDimensions "env"
productFlavors {
dev {
dimension "env"
buildConfigField "String", "GA_GAME_KEY", "\"062fb3ea732ddbd68a5e85be70815856\""
buildConfigField "String", "GA_SECRET_KEY", "\"95f87e95f7e3b4ec7a8df02df02a17e9ff394dbe\""
buildConfigField "String", "GA_BUILD", "\"1.0.0-dev\""
}
prod {
dimension "env"
buildConfigField "String", "GA_GAME_KEY", "\"<prod-key>\""
buildConfigField "String", "GA_SECRET_KEY", "\"<prod-secret>\""
buildConfigField "String", "GA_BUILD", "\"1.0.0\""
}
}
}Then expose BuildConfig.GA_GAME_KEY etc. to JS via a tiny custom module of your own, or via the host app's MainApplication.kt calling GameAnalytics.initialize() natively before the JS layer runs.
Trade-off: zero JS changes, but you can't switch keys at runtime, and you need to maintain a flavor for each environment.
Option C: Fetch keys from your own backend at startup
Best for shipped apps that need to rotate keys or A/B-test different GA projects without releasing a new build.
useEffect(() => {
(async () => {
const res = await fetch('https://config.yourapp.com/analytics-keys');
const { gameKey, secretKey, build } = await res.json();
GameAnalytics.configureBuild(build);
GameAnalytics.initialize(gameKey, secretKey);
})();
}, []);GameAnalytics buffers any events fired before initialize() completes, so an async init is safe.
API reference
Core / session
| Method | Description |
|---|---|
| initialize(gameKey, secretKey) | Initialize the SDK. Call once on mount. |
| configureBuild(build) | Tag the build version. Call before initialize. |
| setEnabledInfoLog(enabled) | Toggle GA's info-level logcat output. |
| setEnabledVerboseLog(enabled) | Toggle GA's verbose-level logcat output. |
Design events
Most common event type. Use colon-delimited IDs (up to 5 segments).
// Plain
GameAnalytics.trackDesignEvent('menu:settings:open');
GameAnalytics.trackDesignEvent('tutorial:step1:complete');
// With value
GameAnalytics.trackDesignEventWithValue('combat:dps', 245.7);
GameAnalytics.trackDesignEventWithValue('level:1:seconds', 42);Progression events
// Just a world
GameAnalytics.trackProgressionEvent('Start', 'world01');
// World + level
GameAnalytics.trackProgressionEvent('Complete', 'world01', 'level03');
// World + level + phase
GameAnalytics.trackProgressionEvent('Fail', 'world01', 'level03', 'boss');
// With a score
GameAnalytics.trackProgressionEventWithScore(
'Complete', 'world01', 'level03', undefined, 1250
);status is typed as 'Start' | 'Complete' | 'Fail' — anything else is rejected at compile time.
Business events (in-app purchases)
amount is in the smallest currency unit (cents).
GameAnalytics.trackBusinessEvent(
'USD',
499, // $4.99
'currency_pack', // itemType
'gem_pack_medium', // itemId
'shop_main' // cartType (optional)
);Resource events (in-game currency)
Track virtual-currency flow. currency and itemType must be pre-registered before initialize (see Pre-init configuration below) — unregistered values are silently dropped.
// Player earned 50 gems from a daily login reward
GameAnalytics.trackResourceEvent('Source', 'gems', 50, 'reward', 'daily_login');
// Player spent 30 gems to revive
GameAnalytics.trackResourceEvent('Sink', 'gems', 30, 'continue', 'revive');flowType is typed 'Source' | 'Sink'.
Ad events
// Impression
GameAnalytics.trackAdEvent('Show', 'RewardedVideo', 'admob', 'between_chapters');
// Click
GameAnalytics.trackAdEvent('Clicked', 'Interstitial', 'admob', 'home_top');
// Failed to show, with a reason
GameAnalytics.trackAdEventWithNoAdReason(
'FailedShow', 'RewardedVideo', 'admob', 'between_chapters', 'NoFill'
);action:'Clicked' | 'Show' | 'FailedShow' | 'RewardReceived' | 'Request' | 'Loaded'adType:'Video' | 'RewardedVideo' | 'Playable' | 'Interstitial' | 'OfferWall' | 'Banner'noAdReason:'Unknown' | 'Offline' | 'NoFill' | 'InternalError' | 'InvalidRequest' | 'UnableToPrecache'
Ad impressions (Impression-Level Ad Revenue / ILRD)
trackAdImpression reports the per-impression revenue your mediation SDK
surfaces in its paid-event callback. One method dispatches to the right
GameAnalytics per-network impression API based on the network argument:
GameAnalytics.trackAdImpression(network, sdkVersion, impressionData);network:'admob' | 'applovinmax' | 'ironsource' | 'fyber' | 'topon'. An unrecognized network is dropped (logged, not thrown) so you can pass values through without a try/catch.sdkVersion: the mediation SDK version string (GA records it on the event).impressionData: the raw, network-specific payload. It's forwarded to GA untyped — converted natively to a JSON object with numeric fields kept numeric — so new fields never require a library update.
AdMob example — wire it straight from the Google Mobile Ads
OnPaidEventListener. AdMobImpressionData documents the fields GA's AdMob
impression schema expects:
import GameAnalytics, {
type AdMobImpressionData,
} from '@renesis_tech/gameanalytics-rn';
const impression: AdMobImpressionData = {
adunit_id: 'ca-app-pub-3940256099942544/1033173712',
currency: 'USD',
precision: 3, // 0 unknown · 1 estimated · 2 publisher-provided · 3 precise
adunit_format: 'interstitial',
network_class_name: 'com.google.ads.mediation.admob.AdMobAdapter',
revenue: 1500, // micros => $0.0015
};
GameAnalytics.trackAdImpression('admob', '23.0.0', impression);For the other networks pass that SDK's impression payload object as-is, e.g.
GameAnalytics.trackAdImpression('applovinmax', sdkVersion, maxAd.toJSON()).
Android only. No-ops on unsupported platforms.
Error events
GameAnalytics.trackErrorEvent('Warning', 'Inventory sync retried');
GameAnalytics.trackErrorEvent('Critical', `Unhandled: ${err.message}`);severity is typed 'Debug' | 'Info' | 'Warning' | 'Error' | 'Critical'.
Custom dimensions
Important: values must be pre-registered in the GameAnalytics dashboard for the corresponding dimension slot. Unregistered values are silently dropped by the SDK.
GameAnalytics.setCustomDimension01('whale');
GameAnalytics.setCustomDimension02('region_emea');
GameAnalytics.setCustomDimension03('ab_test_b');Pass an empty string to clear a dimension.
Global custom event fields
Attach metadata to every event automatically — useful for stamping an A/B variant or subscription tier onto all events without threading them through each call site. The object is converted natively to a JSON object (numeric vs string types preserved).
GameAnalytics.setGlobalCustomEventFields({
ab_variant: 'B',
subscription_tier: 'pro',
sessions: 12,
});Call again to replace the set, or pass {} to clear it.
Pre-init configuration
Call these before initialize. They register the allow-lists the SDK validates events against — values sent later that aren't on these lists are silently dropped.
useEffect(() => {
// Required if you use resource events
GameAnalytics.configureAvailableResourceCurrencies(['gems', 'coins']);
GameAnalytics.configureAvailableResourceItemTypes(['reward', 'continue', 'shop']);
// Optional: allow-list custom-dimension values
GameAnalytics.configureAvailableCustomDimensions01(['free', 'whale']);
GameAnalytics.configureAvailableCustomDimensions02(['region_emea', 'region_us']);
GameAnalytics.configureAvailableCustomDimensions03(['ab_test_a', 'ab_test_b']);
// Optional: tie GA's user id to your own
GameAnalytics.configureUserId(currentUser.id);
GameAnalytics.initialize('YOUR_GAME_KEY', 'YOUR_SECRET_KEY');
}, []);Privacy / GDPR
Disable all event submission when the user opts out (events fired while disabled are dropped):
GameAnalytics.setEnabledEventSubmission(userConsentedToAnalytics);Remote Configs
Read remote-config values set up in the GameAnalytics dashboard — handy for A/B assignment or feature flags. Configs are fetched asynchronously after the first session, so reads return their default until the fetch lands.
import GA from '@renesis_tech/gameanalytics-rn';
GA.initialize(GAME_KEY, SECRET_KEY);
// ... later, after first session ...
if (GA.isRemoteConfigsReady()) {
const variant = GA.getRemoteConfigsValueAsString('SubscriptionTest', '2');
console.log('variant', variant);
}isRemoteConfigsReady()→trueonce the SDK has fetched remote configs at least once this process.getRemoteConfigsValueAsString(key, defaultValue)→ the configured value forkey, ordefaultValuewhen the key isn't configured remotely or remote configs aren't ready yet. The default is required — you do not need to checkisRemoteConfigsReady()first if you're comfortable falling back to it.getRemoteConfigsContentAsString()→ the entire payload as a JSON string for debugging;""if not ready,"{}"if ready with no configs.
Polling-only: there's no listener/event API in this wrapper. Poll
isRemoteConfigsReady()(e.g. a few seconds after launch) if you need to wait before applying a value.
Verifying events on the dashboard
- Open https://tool.gameanalytics.com and pick your game.
- Go to Realtime (top-right) — events appear within ~30 seconds.
- Make sure the Build dropdown matches what you passed to
configureBuild(default0.1).
On-device debug:
adb logcat *:S GameAnalytics:VYou should see lines like Event added to queue: design. Events are flushed in batches every 8 seconds.
How it works under the hood
JS codegen Kotlin
────────────────── ──────────────── ────────────────────────────
GameAnalytics.track… ─▶ NativeGameanalyticsRn ─▶ NativeGameanalyticsRnSpec
(src/index.tsx) (TurboModule spec) (generated abstract class)
│
▼
GameAnalyticsModule.kt
│
▼
io.gameanalytics.sdk.*src/index.tsx— developer-facing API with JSDoc, narrow types, optional args.src/NativeGameanalyticsRn.ts— codegen spec, raw types only.android/.../GameAnalyticsModule.kt— implementation, callscom.gameanalytics.sdk.GameAnalytics.*.android/.../GameAnalyticsPackage.kt— RN package registration.android/build.gradle— pulls incom.gameanalytics.sdk:gameanalytics-android:+from GameAnalytics' Maven repo.
Troubleshooting
initialize() called but currentActivity is null — you called initialize before the first Activity was resumed (e.g. from a module-level statement). Move it into a useEffect.
Events don't appear on the dashboard — confirm the secret key is correct (events are signed and a bad secret causes silent server-side rejection), and check the Build filter on the dashboard. Try setEnabledVerboseLog(true) to see SDK-level debug.
Custom dimensions don't show — values must be allow-listed in the GA dashboard before you send them.
License
MIT © Renesis
