Skip to content

Commit

Permalink
fix: add app_set_id, idfa and idfv support (#458)
Browse files Browse the repository at this point in the history
* fix: do not fetch advertising Id if adid tracking is disabled (#424)

* fix: do not fetch advertising Id if adid tracking is disabled

* fix: ios compilation

* fix: add app_set_id, idfa and idfv support
  • Loading branch information
falconandy authored Jun 29, 2023
1 parent 8f4ea01 commit 00c6cac
Show file tree
Hide file tree
Showing 13 changed files with 107 additions and 26 deletions.
25 changes: 25 additions & 0 deletions examples/plugins/react-native-idfa-plugin/idfaPlugin.ts
Original file line number Diff line number Diff line change
@@ -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<undefined> {
try {
const info = await ReactNativeIdfaAaid.getAdvertisingInfo();
this.idfa = info.id;
} catch (e) {
console.log(e);
}
return undefined;
}

async execute(context: Types.Event): Promise<Types.Event> {
if (this.idfa) {
context.idfa = this.idfa;
}
return context;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -416,5 +421,6 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo

init {
isLocationListening = locationListening
this.shouldTrackAdid = shouldTrackAdid
}
}
2 changes: 1 addition & 1 deletion packages/analytics-react-native/ios/AmplitudeReactNative.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 10 additions & 4 deletions packages/analytics-react-native/ios/AmplitudeReactNative.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ import React
@objc(AmplitudeReactNative)
class ReactNative: NSObject {

private let appleContextProvider = AppleContextProvider()

@objc
static func requiresMainQueueSetup() -> Bool {
return false
}

@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,
Expand All @@ -25,6 +28,9 @@ class ReactNative: NSObject {
"deviceManufacturer": appleContextProvider.deviceManufacturer,
"deviceModel": appleContextProvider.deviceModel,
]
if (trackIdfv) {
applicationContext["idfv"] = appleContextProvider.idfv
}
resolve(applicationContext)
}
}
12 changes: 12 additions & 0 deletions packages/analytics-react-native/ios/AppleContextProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ==
Expand Down
2 changes: 2 additions & 0 deletions packages/analytics-react-native/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const getDefaultConfig = () => {
osName: true,
osVersion: true,
platform: true,
appSetId: true,
idfv: true,
};
return {
cookieExpiration: 365,
Expand Down
12 changes: 9 additions & 3 deletions packages/analytics-react-native/src/plugins/context.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,10 +19,12 @@ type NativeContext = {
deviceModel: string;
carrier: string;
adid: string;
appSetId: string;
idfv: string;
};

export interface AmplitudeReactNative {
getApplicationContext(): Promise<NativeContext>;
getApplicationContext(options: ReactNativeTrackingOptions): Promise<NativeContext>;
}

export class Context implements BeforePlugin {
Expand Down Expand Up @@ -55,7 +57,7 @@ export class Context implements BeforePlugin {

async execute(context: Event): Promise<Event> {
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;
Expand All @@ -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,
Expand All @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions packages/analytics-react-native/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ describe('config', () => {
osName: true,
osVersion: true,
platform: true,
appSetId: true,
idfv: true,
},
transportProvider: new FetchTransport(),
useBatch: false,
Expand Down Expand Up @@ -106,6 +108,8 @@ describe('config', () => {
osName: true,
osVersion: true,
platform: true,
appSetId: true,
idfv: true,
},
transportProvider: new FetchTransport(),
useBatch: false,
Expand Down Expand Up @@ -185,6 +189,8 @@ describe('config', () => {
osName: true,
osVersion: true,
platform: true,
appSetId: true,
idfv: true,
},
transportProvider: new FetchTransport(),
useBatch: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/analytics-react-native/test/helpers/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export const DEFAULT_OPTIONS: Partial<IReactNativeConfig> = {
osName: true,
osVersion: true,
platform: true,
appSetId: true,
idfv: true,
},
transportProvider: {
send: () => Promise.resolve(null),
Expand Down
5 changes: 5 additions & 0 deletions packages/analytics-react-native/test/plugins/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ describe('context', () => {
osName: false,
osVersion: false,
platform: false,
appSetId: false,
idfv: false,
},
userId: '[email protected]',
});
Expand All @@ -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('[email protected]');
Expand Down
1 change: 1 addition & 0 deletions packages/analytics-types/src/base-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
2 changes: 2 additions & 0 deletions packages/analytics-types/src/config/react-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export interface ReactNativeTrackingOptions {
osName?: boolean;
osVersion?: boolean;
platform?: boolean;
appSetId?: boolean;
idfv?: boolean;
}

export interface ReactNativeAttributionOptions {
Expand Down

0 comments on commit 00c6cac

Please sign in to comment.