Skip to content
GitHubGet started

Device data and privacy

Flutter SDK

What the SDK sends in each data tier, how DectogConfig.collect and persistDeviceId gate it, what each option means for the App Store and Play disclosures, the optional advertising-id package, the bundled iOS privacy manifest, and how the collection switch and local storage work.

The SDK sends data in tiers. DectogConfig.collect names what a build collects; the device tier is on by default. The core package never reads an advertising identifier; the separate flutter_dectog_adid package does, for apps that want it.

collect is a Set<DectogCollection>?. Leave it null for the defaults, or pass a set to say exactly what this build collects. A set is exact: a value it leaves out is never collected, whatever the dashboard says.

// Defaults: the device tier, the pasteboard (iOS) and OEM referrers as the
// dashboard sets them, no Apple Search Ads token, no advertising id.
const DectogConfig(apiKey: 'dtg_pub_…');
// Fewer signals: only the store referrers; no device tier, no pasteboard.
// To wait for consent, pass enabled: false instead (see Waiting for consent).
const DectogConfig(
apiKey: 'dtg_pub_…',
collect: {DectogCollection.oemReferrers},
);
Value Default (collect: null) What it sends App Store (App Privacy) Google Play (Data safety)
deviceTier on Device id (IDFV / ANDROID_ID), model, manufacturer, CPU, OS build, screen, text size and Increase Contrast (iOS only), dark mode, Reduce Motion, connectivity, carrier; the device ping Device ID (analytics, linked); Product Interaction Device or other IDs; App activity
pasteboard the dashboard setting, else on for iOS, off for Android A copied DecTog link that names a click, read once at first launch Nothing to declare (no required-reason API); iOS 16 and later shows the paste alert Nothing to declare; Android 12 and later shows the clipboard notice
oemReferrers the dashboard setting, else on The Huawei, Samsung, Xiaomi and vivo store referrers (Android) Not applicable Nothing of its own: the store’s referrer for this install
appleSearchAdsToken off The AdServices attribution token (iOS 14.3 and later); no IDFA, no prompt Advertising Data, for developer advertising Not applicable
adId on once advertisingIdProvider is set The IDFA after App Tracking Transparency, or the GAID Device ID, used for tracking; the ATT prompt Advertising ID; the AD_ID permission

These rows are a starting point for your answers, not legal advice: declare what your app does with the data, including what you send through events.

Field group Fields Source
App app_platform (flutter), app_version, app_build_number, package_name, installer_package, install_time, update_time, first_install_ts, install_state package_info_plus, the package manager (Android), the container and Keychain dates (iOS)
OS os, os_version, language, timezone, utc_offset_min (the current offset from UTC in minutes) dart:io, DateTime and device_info_plus
Referrer install_referrer, referrer_store, referrer_status, referrer_attempts, referrer_wait_ms, the device and server timestamps, install_version, google_play_instant The native referrer readers (Android)
Reliability install_attempt_id, attempt, created_at_client, sent_at_client The SDK’s sentinel
Metadata plugin version, Dart version, platform, build_signature (the Android signing-key hex, when available), every store’s raw referrer read status The SDK

Optional signals ride along when enabled: meta_referrer (with fbAppId), clipboard_url (the pasteboard read), asa_token (with appleSearchAdsToken), ad_id and ad_tracking_limited (with advertisingIdProvider). See Deterministic attribution.

The device tier (deviceTier, on by default)

Section titled “The device tier (deviceTier, on by default)”
Tier Fields Source Server use
Device ID device_id: ANDROID_ID on Android, the IDFV on iOS (Keychain-persisted by default, see below) the plugin’s platform channel install dedupe, reinstall detection, the device ping, event batches, attribution re-pulls
Hardware device_model, device_manufacturer, device_kind (phone or tablet), cpu_type, os_build, is_physical_device device_info_plus device breakdowns, fingerprint inputs, the emulator fraud rule
Screen screen_width, screen_height (physical pixels of the Flutter view), screen_density (device pixel ratio) PlatformDispatcher, no WebView the fingerprint’s screen-profile comparison
Settings text_size (iOS only: the body text size in points for the text-size setting, 17 by default), dark_mode, high_contrast (iOS only: Increase Contrast), reduce_motion (Reduce Motion on iOS, Remove animations on Android) PlatformDispatcher (textScaleFactor, platformBrightness, accessibilityFeatures), no WebView compared with the tap’s settings for 0 points; see Device settings
Network connectivity_type (wifi, cellular, ethernet, vpn, bluetooth, none), carrier_mcc and carrier_mnc (Android only; iOS hides carrier identity) connectivity_plus, TelephonyManager connectivity and carrier breakdowns, the fingerprint windows
Web UA web_user_agent (Android only) WebSettings.getDefaultUserAgent compared, for 0 points, with the OS build and WebView version of a tap from an Android in-app browser

Without deviceTier:

  • none of the fields above is sent, with the install or with in-app clicks (utc_offset_min is not in the tier and is still sent, like timezone);
  • the device ping (POST /api/v1/device-ping, once per UTC day, one row per device and day, which feeds the Overview’s Retention card) is not sent;
  • event batches and app-open reports carry the install_id only, and getAttributionData() re-pulls by install_id instead of device_id;
  • reinstalls cannot be detected by device id, and the fingerprint has fewer signals to corroborate a candidate, so more installs come back as routing only or organic.

The device tier includes four settings: text size and Increase Contrast on iOS only, dark mode and Reduce Motion (Remove animations on Android) on both platforms. The interstitial and tap-to-store link pages read the same settings from the browser, with the UTC offset, and the server compares each with the install’s value; only an exact match counts as agreement. The page reads Increase Contrast in any browser, but an Android install sends none, so on Android it is always missing. Today they score 0 points, so they change no verdict; the server records whether they agree, to measure whether they tell two otherwise identical phones apart. A value the SDK cannot read is left out, and reading them never stops the install from being registered.

What they cost:

  • Apple’s rules. Apple’s user-privacy rules for developers prohibit deriving data from a device’s configuration to identify it, whatever the user consented to. These settings are device configuration, read to tell one phone from another. Nothing is combined into an identifier and nothing is matched across accounts, but whether this is acceptable for your app is your call, and the App Store review risk is yours.
  • Health data. Increase Contrast, Reduce Motion and a large text size can suggest a disability, which EU data-protection law may treat as health data.

An app that cannot carry this has two switches:

  • Leave deviceTier out of collect. The app then sends none of the four settings, and also no device id, model, screen or network fields, so the fingerprint matches fewer installs. It still sends utc_offset_min, which is outside the tier like timezone.
  • Turn Probabilistic match off for the app on the dashboard’s Apps page (Advanced). The fingerprint never runs, nothing is compared, only deterministic signals attribute its installs, and the app’s interstitial and tap-to-store pages no longer read the settings.

With the probabilistic match on, a tap through the interstitial or tap-to-store page records the settings and the UTC offset on the click whether or not the app sends its device tier. They are cleared from the click after 30 days at most, and from the install with the rest of the device tier at the retention cutoff.

identify, trackConversion, trackEvent and trackScreenView send what you pass: customer_id, name, email, phone, revenue, currency, event names and metadata. The server stores the customer’s name, email and phone in plain text on the customer record until the retention cutoff. The dashboard shows a customer’s and an install’s screen views in order, per app session, so keep personal data out of screen names. See Events.

Every recognized link open sends the slug, the opened URL’s utm_* and dec_* values, platform, OS version, language, the device id and model (device tier only) and, on Android, the launching package. The rest of the URL (other query parameters, the fragment) stays on the device. See Initialize and handle links.

The device id is vendor-scoped: Settings.Secure.ANDROID_ID on Android, identifierForVendor (IDFV) on iOS. Neither is an advertising identifier.

persistDeviceId (default true) keeps the first IDFV the SDK reads in the iOS Keychain (service app.dectog.flutter_dectog, account device_id, accessibility AfterFirstUnlockThisDeviceOnly, so it never migrates to another device through a backup). Later reads return the stored value, which is how a reinstall keeps its device id and is reported as a reinstall. When the Keychain is locked (before the first unlock after a reboot) the plain IDFV is returned and nothing is written. With persistDeviceId: false the plain IDFV is sent and changes on reinstall; a reinstall then looks like a new device unless a deterministic signal attributes it. The setting has no effect on Android, where ANDROID_ID already survives reinstall for the same signing key, and no effect at all without the device tier.

The iOS plugin also keeps two install dates in the Keychain (first_install_time, latest_install_time) to classify reinstalls and updates. They are timestamps, not identifiers.

The core SDK does not read or send the Google Advertising ID or the IDFA. It depends on no Play services artifact and declares no AD_ID permission on Android; on iOS it links neither AdSupport nor AppTrackingTransparency, and its privacy manifest sets NSPrivacyTracking to false.

Apps that want the advertising id add the companion package flutter_dectog_adid. It is the only DecTog package that links AdSupport and AppTrackingTransparency (iOS) and Play services ads-identifier with the AD_ID permission (Android):

import 'package:flutter_dectog_adid/flutter_dectog_adid.dart';
await DectogAdId.requestTracking(); // iOS: the ATT prompt, once
await FlutterDectog().initialize(const DectogConfig(
apiKey: 'dtg_pub_…',
advertisingIdProvider: DectogAdId.provider,
));
  • DectogAdId.provider only reads; it never prompts. Ask with requestTracking() before initialize() so the install registration can carry the IDFA. iOS shows the prompt only while the app is active and only when Info.plist has NSUserTrackingUsageDescription.
  • When the user did not allow tracking (ATT not authorized, an Android opt-out, the all-zero id), the install carries ad_tracking_limited: true and no id.
  • The server stores ad_id in its own table, never joined to the device id, and deletes it at the retention cutoff.
  • Its privacy manifest declares the IDFA as a Device ID used for tracking (NSPrivacyTracking true, no tracking domains: the package itself contacts no server). On Android, declare the Advertising ID in the Play Console (Policy, App content, Advertising ID) and in Data safety.
  • A collect set without DectogCollection.adId turns the read off even with a provider.

The Apple Search Ads token (appleSearchAdsToken, off by default) is not an identifier and shows no prompt.

PrivacyInfo.xcprivacy ships with the plugin (as a package resource with Swift Package Manager, as the flutter_dectog_privacy resource bundle with CocoaPods). It declares:

Key Value
NSPrivacyTracking false
NSPrivacyTrackingDomains empty
Collected data types Device ID (linked to the user), Other usage data (not linked), User ID, Purchase history, Name, Email address, Phone number (linked). Every type is for the Analytics purpose and not used for tracking.
Accessed API types NSPrivacyAccessedAPICategoryFileTimestamp, reason C617.1: the install and update dates are read from the creation timestamps of the app’s own executable, Library and Documents directories. No user defaults, disk space or boot time APIs.

The User ID, purchase, name, email and phone entries cover the event tier. The name, email and phone your app passes to identify are sent and stored as given, not hashed; declare them in your App Privacy answers when you pass them. If your app never calls identify with contact fields, those types are declared but never sent.

Declare the collection in your privacy policy and in both store forms (Google Play Data safety, App Store privacy labels) with the purpose “analytics / install attribution”:

  • Device or other IDs: the vendor-scoped device id, persisted across reinstall on iOS when persistDeviceId is on.
  • App info and performance / usage data: install and open events, app version, OS, device model, screen, connectivity, carrier (Android), the UTC offset.
  • Display and accessibility settings, with the device tier: text size and Increase Contrast (iOS only), dark mode and Reduce Motion. The accessibility settings can suggest a disability, which EU law may treat as health data; see Device settings.
  • User IDs, purchase history, contact info: only if you pass them through identify and trackConversion.
  • Advertising ID: only with flutter_dectog_adid (see above).
  • Location: none. The SDK sends no location; the server derives country and ASN from the request’s network address.

If your consent regime is opt-in, start with DectogConfig(enabled: false) and call setEnabled(true) once the user agrees; see Waiting for consent. Leaving deviceTier out of collect only drops the device tier, the four device settings with it.

await dectog.initialize(DectogConfig(apiKey: '…', enabled: false));
// … the user agrees:
await dectog.setEnabled(true);

enabled: false holds collection until the app calls setEnabled(true). The app’s choice is stored, so from then on it wins over the config on every launch: a user who agreed once is not asked again, and one who said no stays off.

While collection is off, the SDK makes no request of its own:

  • no click, app-open, install, claim, attribution, settings, resolve or device-ping request, and no pasteboard read;
  • events you track are held in the on-disk queue, up to the queue’s 7 days;
  • a link that opens the app still reaches onLinkClick, as parsed from the URL: slug, username, clickParams and the query, without linkResolution. Its click and open are held in memory for this process.

setEnabled(true) then sends the held events, registers the install if it never was, and reports the held links with the time they arrived. The store referrer still attributes the install deterministically; the fingerprint window has usually passed by then. Events held longer than 7 days are dropped, never sent.

Calls your app makes itself still go out while collection is off: createLink, getLinks, createTempLink, createReferralLink, resolveLink and the other link-management methods. They carry no device data.

await dectog.setEnabled(false); // persisted across launches
final on = await dectog.isEnabled();
await dectog.setEnabled(true); // resume

setEnabled(false) stops the same requests as above, from the next call on; a request already on its way completes. Events stay held on the device, and link reports held since the last consent are dropped, so a later setEnabled(true) sends the events but never reports a link that arrived before the user said no.

Everything the SDK persists lives in one private, backup-excluded directory:

Platform Directory
Android Context.getNoBackupFilesDir()/dectog/
iOS Application Support /dectog/, flagged NSURLIsExcludedFromBackupKey
Fallback Application Support, or the documents directory when path_provider fails
File Contents
.dectog_install.json The install sentinel: attempt id, status, attempts, timestamps, server install id, referrer and Apple Search Ads delivery flags
.dectog_sdk_state.json Customer id, the last identify fingerprint, the enabled flag, the install id
.dectog_event_queue.jsonl Queued events, one JSON object per line

Uninstalling the app removes all three. A restored backup never contains them, so a restored app registers as a fresh install.

Every authenticated call carries the API key as a Bearer token plus an HMAC-SHA256 signature over the timestamp and the body (x-dectog-timestamp, x-dectog-signature: v1=<hex>), a User-Agent of DectogSDK/<version> (<platform>), and a 30-second timeout. The server rejects unsigned requests and timestamps more than 30 minutes off. Requests are plain HTTPS to effectiveBaseUrl; the public resolve call is the only unsigned one.

After the retention window (400 days by default on the hosted service, shorter if the account sets its own), the server deletes the raw clicks, installs, events and app opens once its daily per-link counts hold their day, blanking their device fields, IPs and user agents until then, and blanks customers’ contact details. What is kept and when it is removed is listed under Privacy and data.