TT Festival App — Tracker & SDK Audit + First-Launch Tracking Graph
Scope:
com.appmiral.ttfestivalv2.0.1 (versionCode 5), decompiled with jadx, cross-checked against a live capture on a rooted Pixel 10a. Companion docs:crowd-connected.md,crowd-connected-faq.md,reverse-engineering.md,hardcoded-credentials-and-keys.md.Date of live capture: 2026-06-08.
This document answers three questions:
- Which trackers/SDKs are actually in the app? (and which are only registered capability of the Appmiral platform but not bundled here)
- What does each one send, where, and when — pre-consent vs post-consent?
- What actually happens on a fresh first launch — the empirical “tracking graph” captured on a real device.
1. The two layers: registered vs bundled
The app is built on the white-label Appmiral platform. Appmiral ships a
generic consent registry (com.appmiral.consent.Sdk) that enumerates every
SDK the platform can integrate across all its customer festivals — 18 SDKs.
That registry is not the list of SDKs in this build. It is the menu; each
festival build only bundles a subset.
To separate menu from reality we checked which vendor packages actually exist in the decompiled sources / native libs.
1.1 Consent registry (platform capability — Sdk.java)
SDK id (SDK.*) |
Consent category | Bundled in TT Festival v2.0.1? |
|---|---|---|
APPMIRAL.ANALYTICS |
Analytics | ✅ yes (AWS Lambda analytics) |
APPMIRAL.PROFILE |
Necessary | ✅ yes |
AWS.Cognito |
Necessary | ✅ yes |
AWS.SNS |
Necessary | ✅ yes |
CM.Push |
Necessary | ✅ yes (com.cm.cmpush, 62 files) |
CM.CDP |
Analytics | ✅ yes (CmCdpAnalytics) |
Crowdconnected.Core |
Functional | ✅ yes (net.crowdconnected, 89 files + libpositioner.so) |
Firebase.Analytics |
Analytics | ✅ yes |
Firebase.Crashlytics |
Necessary | ✅ yes |
Facebook.Core |
Social | ❌ not bundled (placeholder) |
Braze.Analytics / Braze.Core / Braze.Location |
Analytics / Necessary / Location | ❌ not bundled |
Insider.Analytics / Insider.Core |
Analytics | ❌ not bundled |
Permutive.Analytics |
Analytics | ❌ not bundled |
Salesforce |
Necessary | ❌ not bundled |
Tealium.Analytics |
Analytics | ❌ not bundled |
⚠️ Correction to earlier notes. An earlier draft of
reverse-engineering.mdlisted “Salesforce, Braze, Permutive, Tealium, Facebook Connect, Ticketmaster Ignite” as observed SDKs. They are not bundled in this build — they only appear as registry entries and/or as string/manifest placeholders. The corrected, evidence-based inventory is below.
1.2 What is actually bundled (verified by package presence)
| Vendor / package | Files | Role |
|---|---|---|
com.amazonaws (AWS SDK) |
~1,813 | Cognito identity + SNS push + S3/mobile backend |
com.google / Firebase |
many | Installations, Crashlytics, Analytics, Sessions, GMS, Maps |
net.crowdconnected + libpositioner.so |
89 + native | Crowd/indoor positioning analytics |
com.cookieinformation |
139 | Consent Management Platform (CMP) |
com.cm.cmpush |
62 | CM.com push messaging |
co.novemberfive.android.analytics.* |
— | Analytics fan-out wrapper (Firebase + Lambda + CM CDP) |
Not present (directory absent in sources): com.facebook, com.braze,
com.useinsider, com.permutive, com.salesforce, com.tealium.
2. The analytics fan-out (how events are routed)
Events do not call vendors directly. They go through a November Five wrapper:
App code → MultiTracker → ConsentGatedTracker(s) ─┬─ AnalyticsFirebase (Firebase Analytics)
├─ LambdaAnalytics (→ AWS Lambda / Appmiral)
├─ CmCdpAnalytics (→ CM.com CDP)
└─ DataConnectorManager (0 connectors bundled → no-op)
com.appmiral.base.analytics.ConsentGatedTrackerwraps each analytics tracker and drops events until the matching consent category is granted.DataConnectorManageris a pluggable framework that loads connector classes by reflection from a server-driven list (com.appmiral.dataconnectors.*). In this build no connector classes are bundled, so it is inert.- This means the analytics path (Firebase events, CM CDP, Lambda) is genuinely consent-gated. The catch is everything that is classified “Necessary” — that runs regardless (see §3 + §4).
3. Per-SDK: what is sent, where, and when
Legend for When: 🔴 = fires at launch before any consent · 🟢 = only after the matching consent category is granted.
| SDK | Category | Destination host(s) | Data | When |
|---|---|---|---|---|
| Firebase Installations | (infra) | firebaseinstallations.googleapis.com |
Firebase Installation ID (FID), app/package, platform | 🔴 |
| Firebase Crashlytics | Necessary | firebase-settings.crashlytics.com (+ crash upload host) |
Settings fetch + version-control info at init; crash reports gated by collection flag | 🔴 init / 🟢 report upload |
| Firebase Analytics | Analytics | Google FA endpoints | Screen/event analytics | 🟢 |
| Google GMS | (infra) | android.apis.google.com |
Play/GMS plumbing, attribution | 🔴 |
| AWS Cognito (identity pool) | Necessary | cognito-identity.eu-west-1.amazonaws.com |
Issues an anonymous Cognito identity id + temporary AWS creds | 🔴 |
| AWS SNS | Necessary | sns.eu-west-1.amazonaws.com |
Registers a push endpoint (device token → SNS endpoint ARN) | 🔴 |
| Appmiral API / media | Necessary / content | app.appmiral.com, media.appmiral.com (Cloudflare) |
App config, festival content, profile | 🔴 |
| CM.com Push | Necessary | CM push endpoints | Push token registration | 🔴 (when used) |
| CM.com CDP | Analytics | CM CDP endpoints | Customer-data-platform events | 🟢 |
| Appmiral Lambda analytics | Analytics | app.appmiral.com/function/ (AWS Lambda) |
First-party analytics events | 🟢 |
| Crowd Connected | Functional | Crowd Connected ingest (see crowd-connected.md) |
Bluetooth/Wi-Fi/GPS positioning samples | 🟢 + requires location permission + active zone |
Key observation: the four most identity-revealing network calls (Firebase Installations, Crashlytics settings, Cognito identity, SNS push registration) are all categorised by the app itself as infrastructure or “Necessary”, and therefore fire at launch before the consent dialog is ever shown — and, as the capture below proves, even while the device is locked.
4. Empirical tracking graph — fresh first launch
Two captures were made on 2026-06-08:
- Locked capture — proves pre-consent phone-home can happen before the user can interact with the app.
- Unlocked capture — proves what the visible onboarding says and when Crowd Connected starts appearing in traffic after the user proceeds through the intake.
Raw proof for both captures lives in the session files/proof/ folder and is
not committed, because it contains device/account identifiers.
4.1 Locked run: pre-consent, behind keyguard
Method. On a rooted Pixel 10a (Magisk), the app data was wiped
(pm clear com.appmiral.ttfestival), DNS cache flushed, then the app was launched
once while a per-app tcpdump + ss poller attributed every socket to the app
PID. No consent was given. The device screen stayed locked the whole time
(fingerprint; the operator was not present), yet the app process started behind
the keyguard and its SDKs initialised and phoned home. Capture epoch:
1780874931.76 → 1780875034.62. Raw proof lives in the session
files/proof/ folder (ttf_dns.txt, ttf_conns.txt, ttf_sni.txt,
ttf_logcat.txt, ttf.pcap) and is not committed (it contains device
identifiers).
DNS timeline (app PID 24258, “iral.ttfestival”)
| T (clock) | T+ | Resolved host | Belongs to |
|---|---|---|---|
| 01:28:52.16 | +0.0 s | firebaseinstallations.googleapis.com |
Firebase Installations |
| 01:28:52.23 | +0.07 s | firebase-settings.crashlytics.com |
Firebase Crashlytics |
| 01:28:52.64 | +0.5 s | android.apis.google.com |
Google GMS |
| 01:28:54.27 | +2.1 s | cognito-identity.eu-west-1.amazonaws.com |
AWS Cognito |
| 01:28:54.57 | +2.4 s | sns.eu-west-1.amazonaws.com |
AWS SNS |
(connectivitycheck.gstatic.com and quake-pa.googleapis.com later in the trace
are OS-level traffic, not the app.)
Attributed TCP connections (all port 443, owned by the app PID)
| Remote IP | Resolves to | State at first poll |
|---|---|---|
52.50.25.135 |
AWS Cognito identity (eu-west-1) | ESTAB ×2 |
3.251.106.215 |
AWS SNS (eu-west-1) | CLOSE-WAIT, Recv-Q=25 → exchange completed |
104.18.37.205 |
app.appmiral.com (Cloudflare) |
ESTAB |
172.64.150.51 |
media.appmiral.com (Cloudflare) |
ESTAB |
(The local address was a private 192.168.x.x LAN IP — redacted.)
Logcat corroboration (init order)
01:28:52.099 Start proc 24258 for com.appmiral.ttfestival/…SplashActivity
01:28:52.139 FirebaseApp initializing all Firebase APIs
01:28:52.140 FirebaseSessions init
01:28:52.141 FirebaseCrashlytics 19.4.3 init
01:28:52.146 FirebaseInitProvider successful
01:28:52.161 Crashlytics "Saved version control info"
01:28:52.169 FA (Firebase Analytics) present
AppmiralApp.onCreate → AppmiralAnalyticsEnvironment.initializeTrackers
→ gamification.ModuleTracker (+ other module trackers)
Graph
flowchart LR
A[App launch<br/>pm clear, no consent<br/>device LOCKED] --> B[Firebase init T+0.04s]
B --> B1[firebaseinstallations.googleapis.com<br/>FID]
B --> B2[firebase-settings.crashlytics.com<br/>settings + version info]
B --> B3[android.apis.google.com<br/>GMS]
A --> C[AWS init T+2s]
C --> C1[cognito-identity.eu-west-1<br/>identity id + temp creds]
C --> C2[sns.eu-west-1<br/>push endpoint registration]
A --> D[Appmiral backend]
D --> D1[app.appmiral.com<br/>config / profile / Lambda]
D --> D2[media.appmiral.com<br/>content]
classDef pre fill:#ffd5d5,stroke:#c00;
class B1,B2,B3,C1,C2,D1,D2 pre
All red nodes fired before consent and behind the lock screen.
4.2 Unlocked run: visible onboarding + later Crowd Connected contact
After the phone owner unlocked the device, the same fresh-run harness was run
again. This time screenshots and UI XML could be captured. The first visible
screen after pm clear was:
WIJ RESPECTEREN JE PRIVACY
“We nemen privacy serieus en zullen uw persoonlijke gegevens alleen gebruiken in overeenstemming met onze Privacy Statement. We doen dit om u een betere totaal ervaring te bieden.”
Checkbox: “Ik begrijp en sta toe dat deze app mijn informatie gebruikt voor een optimale werking van de app.”
Button: DOORGAAN
The next visible intake step was profile completion (name/email) with a separate marketing-email opt-in. The proof screenshots/XML are:
| Proof file | Meaning |
|---|---|
unlocked_20260608_0141/01_fresh_launch_t30.png + .xml |
privacy-intake screen |
unlocked_20260608_0141/02_current_location_push.png + .xml |
profile-completion screen |
unlocked_20260608_0141/03_current_after_capture.png + .xml |
app main screen after onboarding |
DNS timeline from the unlocked run
| T (clock) | Resolved host | Meaning |
|---|---|---|
| 01:42:18.28 | firebaseinstallations.googleapis.com |
Firebase Installation ID |
| 01:42:18.36 | firebase-settings.crashlytics.com |
Crashlytics settings |
| 01:42:18.45 | app.appmiral.com |
Appmiral API/config/profile |
| 01:42:18.55 | media.appmiral.com |
Appmiral media/content |
| 01:42:18.76 | android.apis.google.com |
Google/GMS |
| 01:42:20.40 | cognito-identity.eu-west-1.amazonaws.com |
AWS Cognito identity |
| 01:42:20.68 | sns.eu-west-1.amazonaws.com |
AWS SNS push endpoint |
| 01:43:32.82 | cognito-idp.us-east-1.amazonaws.com |
Cognito user-pool/profile/auth flow |
| 01:43:33.67 | mobile.crowdconnected.com |
Crowd Connected device/API contact |
This confirms the locked-run finding and adds a second phase: once onboarding continues while unlocked, Crowd Connected appears on the wire. The capture does not include TLS bodies, so it proves destination and timing, not payload content.
The Android permission log also confirms that the notification prompt was shown and accepted at 01:44:02:
GrantPermissionsViewModel: ... permissionGroupName=android.permission-group.NOTIFICATIONS
Permission grant result ... permission=android.permission.POST_NOTIFICATIONS
5. What this means (and what it does not)
SDK initialisation ≠ analytics collection. Two things are true at once:
-
✅ Consent-gating is real for the analytics layer. Firebase Analytics, CM CDP and Appmiral Lambda events are wrapped in
ConsentGatedTracker, and the manifest shipsfirebase_analytics_collection_enabled=falseandfirebase_crashlytics_collection_enabled=false. So behavioural analytics events and crash report uploads should not flow until consent. -
🔴 But identity/infrastructure phone-home is not gated. Because the app classifies Cognito, SNS, CM.Push, Crashlytics and Appmiral.Profile as “Necessary”, those handshakes run at launch — creating an anonymous AWS identity, registering a push endpoint, and fetching a Firebase Installation ID before the user has agreed to anything and before they have even unlocked the phone. From a GDPR/ePrivacy standpoint, “necessary” is doing a lot of work here: a push-endpoint registration and a cloud identity are arguably not strictly necessary to display a festival programme.
5.1 Consent-config red flags (from strings.xml)
| Setting | Value | Implication |
|---|---|---|
cookieInformationClientId / Secret / SolutionId |
null |
The bundled Cookie Information CMP is unconfigured |
showDenyCookiesButton |
false |
The “deny” button is hidden in the cookie UI (dark-pattern signal) |
facebookAppId / facebookClientToken |
null |
Facebook is a placeholder, not active |
firebase_analytics_collection_enabled |
false |
Analytics collection off until enabled |
firebase_crashlytics_collection_enabled |
false |
Crashlytics upload off until enabled |
6. Limitation / honesty note
The operator (phone owner) was not available during this capture and the device remained locked (fingerprint), so no in-app onboarding screenshots could be taken — only the lock screen was visible. This is actually a stronger finding for the pre-consent question: the network/host capture proves the app phones home before the user can even interact with it. No TLS decryption was performed (no MITM/system CA), so payload bodies were not inspected — only hostnames, IPs, SNI and connection state. That is sufficient to establish who is contacted when, which is the privacy-relevant question, without intercepting account contents.