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.
Choosing what to collect
Section titled “Choosing what to collect”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.
What is sent
Section titled “What is sent”Always, with the install registration
Section titled “Always, with the install registration”| 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_minis not in the tier and is still sent, liketimezone); - 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_idonly, andgetAttributionData()re-pulls byinstall_idinstead ofdevice_id; - reinstalls cannot be detected by device id, and the fingerprint has fewer signals to corroborate a candidate, so more installs come back as
routingonly or organic.
Device settings
Section titled “Device settings”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
deviceTierout ofcollect. 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 sendsutc_offset_min, which is outside the tier liketimezone. - 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.
Post-install events
Section titled “Post-install events”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.
In-app clicks and opens
Section titled “In-app clicks and opens”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 identifier
Section titled “The device identifier”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 advertising identifier
Section titled “The advertising identifier”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, onceawait FlutterDectog().initialize(const DectogConfig( apiKey: 'dtg_pub_…', advertisingIdProvider: DectogAdId.provider,));DectogAdId.provideronly reads; it never prompts. Ask withrequestTracking()beforeinitialize()so the install registration can carry the IDFA. iOS shows the prompt only while the app is active and only whenInfo.plisthasNSUserTrackingUsageDescription.- When the user did not allow tracking (ATT not authorized, an Android opt-out, the all-zero id), the install carries
ad_tracking_limited: trueand no id. - The server stores
ad_idin 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 (
NSPrivacyTrackingtrue, 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
collectset withoutDectogCollection.adIdturns the read off even with a provider.
The Apple Search Ads token (appleSearchAdsToken, off by default) is not an identifier and shows no prompt.
The iOS privacy manifest
Section titled “The iOS privacy manifest”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.
Store declarations
Section titled “Store declarations”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
persistDeviceIdis 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
identifyandtrackConversion. - 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.
Waiting for consent
Section titled “Waiting for consent”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,clickParamsand the query, withoutlinkResolution. 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.
Turning collection off
Section titled “Turning collection off”await dectog.setEnabled(false); // persisted across launchesfinal on = await dectog.isEnabled();await dectog.setEnabled(true); // resumesetEnabled(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.
Local storage
Section titled “Local storage”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.
Transport
Section titled “Transport”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.
Retention on the server
Section titled “Retention on the server”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.