diff --git a/examples/plugins/react-native-idfa-plugin/idfaPlugin.ts b/examples/plugins/react-native-idfa-plugin/idfaPlugin.ts new file mode 100644 index 000000000..e6e12c116 --- /dev/null +++ b/examples/plugins/react-native-idfa-plugin/idfaPlugin.ts @@ -0,0 +1,25 @@ +import { Types } from '@amplitude/analytics-react-native'; +import ReactNativeIdfaAaid from '@sparkfabrik/react-native-idfa-aaid'; + +export default class IdfaPlugin implements Types.BeforePlugin { + name = 'idfa'; + type = 'before' as const; + idfa: string | null = null; + + async setup(_config: Types.Config): Promise { + try { + const info = await ReactNativeIdfaAaid.getAdvertisingInfo(); + this.idfa = info.id; + } catch (e) { + console.log(e); + } + return undefined; + } + + async execute(context: Types.Event): Promise { + if (this.idfa) { + context.idfa = this.idfa; + } + return context; + } +} diff --git a/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AmplitudeReactNativeModule.kt b/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AmplitudeReactNativeModule.kt index 6e8d4d13d..e8fb30c16 100644 --- a/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AmplitudeReactNativeModule.kt +++ b/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AmplitudeReactNativeModule.kt @@ -13,25 +13,33 @@ const val MODULE_NAME = "AmplitudeReactNative" @ReactModule(name = MODULE_NAME) class AmplitudeReactNativeModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { - private val androidContextProvider = AndroidContextProvider(reactContext.applicationContext, false) + private var androidContextProvider: AndroidContextProvider? = null override fun getName(): String { return MODULE_NAME } @ReactMethod - private fun getApplicationContext(promise: Promise) { + private fun getApplicationContext(options: ReadableMap, promise: Promise) { + val trackAdid = if (options.hasKey("adid")) options.getBoolean("adid") else false + if (androidContextProvider == null) { + androidContextProvider = AndroidContextProvider(reactContext.applicationContext, false, trackAdid) + } + promise.resolve(WritableNativeMap().apply { - putString("version", androidContextProvider.versionName) - putString("platform", androidContextProvider.platform) - putString("language", androidContextProvider.language) - putString("osName", androidContextProvider.osName) - putString("osVersion", androidContextProvider.osVersion) - putString("deviceBrand", androidContextProvider.brand) - putString("deviceManufacturer", androidContextProvider.manufacturer) - putString("deviceModel", androidContextProvider.model) - putString("carrier", androidContextProvider.carrier) - putString("adid", androidContextProvider.advertisingId) + putString("version", androidContextProvider!!.versionName) + putString("platform", androidContextProvider!!.platform) + putString("language", androidContextProvider!!.language) + putString("osName", androidContextProvider!!.osName) + putString("osVersion", androidContextProvider!!.osVersion) + putString("deviceBrand", androidContextProvider!!.brand) + putString("deviceManufacturer", androidContextProvider!!.manufacturer) + putString("deviceModel", androidContextProvider!!.model) + putString("carrier", androidContextProvider!!.carrier) + if (trackAdid) { + putString("adid", androidContextProvider!!.advertisingId) + } + putString("appSetId", androidContextProvider!!.appSetId) }) } } diff --git a/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AndroidContextProvider.kt b/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AndroidContextProvider.kt index e414cb3cd..11297ad8a 100644 --- a/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AndroidContextProvider.kt +++ b/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AndroidContextProvider.kt @@ -20,8 +20,9 @@ import java.util.Locale import java.util.UUID import kotlin.collections.ArrayList -class AndroidContextProvider(private val context: Context, locationListening: Boolean) { +class AndroidContextProvider(private val context: Context, locationListening: Boolean, shouldTrackAdid: Boolean) { var isLocationListening = true + var shouldTrackAdid = true private var cachedInfo: CachedInfo? = null private get() { if (field == null) { @@ -34,7 +35,7 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo * Internal class serves as a cache */ inner class CachedInfo { - var advertisingId: String + var advertisingId: String? val country: String? val versionName: String? val osName: String @@ -201,7 +202,11 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo return locale.language } - private fun fetchAdvertisingId(): String { + private fun fetchAdvertisingId(): String? { + if (!shouldTrackAdid) { + return null + } + // This should not be called on the main thread. return if ("Amazon" == fetchManufacturer()) { fetchAndCacheAmazonAdvertisingId @@ -237,14 +242,14 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo return appSetId } - private val fetchAndCacheAmazonAdvertisingId: String + private val fetchAndCacheAmazonAdvertisingId: String? private get() { val cr = context.contentResolver limitAdTrackingEnabled = Secure.getInt(cr, SETTING_LIMIT_AD_TRACKING, 0) == 1 advertisingId = Secure.getString(cr, SETTING_ADVERTISING_ID) return advertisingId } - private val fetchAndCacheGoogleAdvertisingId: String + private val fetchAndCacheGoogleAdvertisingId: String? private get() { try { val AdvertisingIdClient = Class @@ -340,7 +345,7 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo get() = cachedInfo!!.country val language: String get() = cachedInfo!!.language - val advertisingId: String + val advertisingId: String? get() = cachedInfo!!.advertisingId val appSetId: String get() = cachedInfo!!.appSetId // other causes// failed to get providers list @@ -416,5 +421,6 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo init { isLocationListening = locationListening + this.shouldTrackAdid = shouldTrackAdid } } diff --git a/packages/analytics-react-native/ios/AmplitudeReactNative.m b/packages/analytics-react-native/ios/AmplitudeReactNative.m index 1700bba9b..8f3aa47f2 100644 --- a/packages/analytics-react-native/ios/AmplitudeReactNative.m +++ b/packages/analytics-react-native/ios/AmplitudeReactNative.m @@ -2,6 +2,6 @@ @interface RCT_EXTERN_MODULE(AmplitudeReactNative, NSObject) -RCT_EXTERN_METHOD(getApplicationContext: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(getApplicationContext: (NSDictionary*)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) @end diff --git a/packages/analytics-react-native/ios/AmplitudeReactNative.swift b/packages/analytics-react-native/ios/AmplitudeReactNative.swift index 60432835d..282a9aba4 100644 --- a/packages/analytics-react-native/ios/AmplitudeReactNative.swift +++ b/packages/analytics-react-native/ios/AmplitudeReactNative.swift @@ -4,8 +4,6 @@ import React @objc(AmplitudeReactNative) class ReactNative: NSObject { - private let appleContextProvider = AppleContextProvider() - @objc static func requiresMainQueueSetup() -> Bool { return false @@ -13,10 +11,15 @@ class ReactNative: NSObject { @objc func getApplicationContext( - _ resolve: RCTPromiseResolveBlock, + _ options: NSDictionary, + resolver resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock ) -> Void { - let applicationContext: [String: String?] = [ + let trackingOptions = options as! [String: Bool] + let trackIdfv = trackingOptions["idfv"] ?? false + let appleContextProvider = AppleContextProvider(trackIdfv: trackIdfv) + + var applicationContext: [String: String?] = [ "version": appleContextProvider.version, "platform": appleContextProvider.platform, "language": appleContextProvider.language, @@ -25,6 +28,9 @@ class ReactNative: NSObject { "deviceManufacturer": appleContextProvider.deviceManufacturer, "deviceModel": appleContextProvider.deviceModel, ] + if (trackIdfv) { + applicationContext["idfv"] = appleContextProvider.idfv + } resolve(applicationContext) } } diff --git a/packages/analytics-react-native/ios/AppleContextProvider.swift b/packages/analytics-react-native/ios/AppleContextProvider.swift index 12367d511..31ca43706 100644 --- a/packages/analytics-react-native/ios/AppleContextProvider.swift +++ b/packages/analytics-react-native/ios/AppleContextProvider.swift @@ -9,6 +9,14 @@ import Foundation public let osVersion: String = AppleContextProvider.getOsVersion() public let deviceManufacturer: String = AppleContextProvider.getDeviceManufacturer() public let deviceModel: String = AppleContextProvider.getDeviceModel() + public var idfv: String? = nil + + init(trackIdfv: Bool) { + super.init() + if (trackIdfv) { + fetchIdfv() + } + } private static func getVersion() -> String? { return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String @@ -41,6 +49,10 @@ import Foundation return String(bytes: Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN)), encoding: .ascii)!.trimmingCharacters(in: .controlCharacters) } + private func fetchIdfv() { + self.idfv = UIDevice.current.identifierForVendor?.uuidString + } + private static func getDeviceModel() -> String { let platform = getPlatformString() // == iPhone == diff --git a/packages/analytics-react-native/src/config.ts b/packages/analytics-react-native/src/config.ts index 6b8b9880e..d0c80e192 100644 --- a/packages/analytics-react-native/src/config.ts +++ b/packages/analytics-react-native/src/config.ts @@ -23,6 +23,8 @@ export const getDefaultConfig = () => { osName: true, osVersion: true, platform: true, + appSetId: true, + idfv: true, }; return { cookieExpiration: 365, diff --git a/packages/analytics-react-native/src/plugins/context.ts b/packages/analytics-react-native/src/plugins/context.ts index 7d83e8294..80f449297 100644 --- a/packages/analytics-react-native/src/plugins/context.ts +++ b/packages/analytics-react-native/src/plugins/context.ts @@ -1,4 +1,4 @@ -import { BeforePlugin, ReactNativeConfig, Event } from '@amplitude/analytics-types'; +import { BeforePlugin, ReactNativeConfig, Event, ReactNativeTrackingOptions } from '@amplitude/analytics-types'; import UAParser from '@amplitude/ua-parser-js'; import { UUID } from '@amplitude/analytics-core'; import { getLanguage } from '@amplitude/analytics-client-common'; @@ -19,10 +19,12 @@ type NativeContext = { deviceModel: string; carrier: string; adid: string; + appSetId: string; + idfv: string; }; export interface AmplitudeReactNative { - getApplicationContext(): Promise; + getApplicationContext(options: ReactNativeTrackingOptions): Promise; } export class Context implements BeforePlugin { @@ -55,7 +57,7 @@ export class Context implements BeforePlugin { async execute(context: Event): Promise { const time = new Date().getTime(); - const nativeContext = await this.nativeModule?.getApplicationContext(); + const nativeContext = await this.nativeModule?.getApplicationContext(this.config.trackingOptions); const appVersion = nativeContext?.version || this.config.appVersion; const platform = nativeContext?.platform || BROWSER_PLATFORM; const osName = nativeContext?.osName || this.uaResult.browser.name; @@ -65,6 +67,8 @@ export class Context implements BeforePlugin { const language = nativeContext?.language || getLanguage(); const carrier = nativeContext?.carrier; const adid = nativeContext?.adid; + const appSetId = nativeContext?.appSetId; + const idfv = nativeContext?.idfv; const event: Event = { user_id: this.config.userId, @@ -81,6 +85,8 @@ export class Context implements BeforePlugin { ...(this.config.trackingOptions.carrier && { carrier: carrier }), ...(this.config.trackingOptions.ipAddress && { ip: IP_ADDRESS }), ...(this.config.trackingOptions.adid && { adid: adid }), + ...(this.config.trackingOptions.appSetId && { android_app_set_id: appSetId }), + ...(this.config.trackingOptions.idfv && { idfv: idfv }), insert_id: UUID(), partner_id: this.config.partnerId, plan: this.config.plan, diff --git a/packages/analytics-react-native/test/config.test.ts b/packages/analytics-react-native/test/config.test.ts index 458ba04d6..cc2470b29 100644 --- a/packages/analytics-react-native/test/config.test.ts +++ b/packages/analytics-react-native/test/config.test.ts @@ -55,6 +55,8 @@ describe('config', () => { osName: true, osVersion: true, platform: true, + appSetId: true, + idfv: true, }, transportProvider: new FetchTransport(), useBatch: false, @@ -106,6 +108,8 @@ describe('config', () => { osName: true, osVersion: true, platform: true, + appSetId: true, + idfv: true, }, transportProvider: new FetchTransport(), useBatch: false, @@ -185,6 +189,8 @@ describe('config', () => { osName: true, osVersion: true, platform: true, + appSetId: true, + idfv: true, }, transportProvider: new FetchTransport(), useBatch: false, diff --git a/packages/analytics-react-native/test/helpers/default.ts b/packages/analytics-react-native/test/helpers/default.ts index ebf714a4b..6ebf51175 100644 --- a/packages/analytics-react-native/test/helpers/default.ts +++ b/packages/analytics-react-native/test/helpers/default.ts @@ -38,6 +38,8 @@ export const DEFAULT_OPTIONS: Partial = { osName: true, osVersion: true, platform: true, + appSetId: true, + idfv: true, }, transportProvider: { send: () => Promise.resolve(null), diff --git a/packages/analytics-react-native/test/plugins/context.test.ts b/packages/analytics-react-native/test/plugins/context.test.ts index 679386bec..65e6ae06b 100644 --- a/packages/analytics-react-native/test/plugins/context.test.ts +++ b/packages/analytics-react-native/test/plugins/context.test.ts @@ -72,6 +72,8 @@ describe('context', () => { osName: false, osVersion: false, platform: false, + appSetId: false, + idfv: false, }, userId: 'user@amplitude.com', }); @@ -92,6 +94,9 @@ describe('context', () => { expect(firstContextEvent.os_version).toBeUndefined(); expect(firstContextEvent.language).toBeUndefined(); expect(firstContextEvent.ip).toBeUndefined(); + expect(firstContextEvent.adid).toBeUndefined(); + expect(firstContextEvent.android_app_set_id).toBeUndefined(); + expect(firstContextEvent.idfv).toBeUndefined(); expect(firstContextEvent.device_id).toEqual('deviceId'); expect(firstContextEvent.session_id).toEqual(1); expect(firstContextEvent.user_id).toEqual('user@amplitude.com'); diff --git a/packages/analytics-types/src/base-event.ts b/packages/analytics-types/src/base-event.ts index 615667299..8585c3fb3 100644 --- a/packages/analytics-types/src/base-event.ts +++ b/packages/analytics-types/src/base-event.ts @@ -47,5 +47,6 @@ export interface EventOptions { ingestion_metadata?: IngestionMetadataEventProperty; partner_id?: string; user_agent?: string; + android_app_set_id?: string; extra?: { [key: string]: any }; } diff --git a/packages/analytics-types/src/config/react-native.ts b/packages/analytics-types/src/config/react-native.ts index e75070e6c..419cdf677 100644 --- a/packages/analytics-types/src/config/react-native.ts +++ b/packages/analytics-types/src/config/react-native.ts @@ -34,6 +34,8 @@ export interface ReactNativeTrackingOptions { osName?: boolean; osVersion?: boolean; platform?: boolean; + appSetId?: boolean; + idfv?: boolean; } export interface ReactNativeAttributionOptions {