Skip to the content.

TT Festival App — Tracker & SDK Audit + First-Launch Tracking Graph

Scope: com.appmiral.ttfestival v2.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:

  1. Which trackers/SDKs are actually in the app? (and which are only registered capability of the Appmiral platform but not bundled here)
  2. What does each one send, where, and when — pre-consent vs post-consent?
  3. 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.

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.md listed “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)

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:

  1. Locked capture — proves pre-consent phone-home can happen before the user can interact with the app.
  2. 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.

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:

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.