Skip to content

Commit 67d9321

Browse files
authored
Merge pull request #198 from segmentio/fix-idfa-not-populating
fix: idfa not populating for initial events
2 parents fd60a77 + 328359b commit 67d9321

3 files changed

Lines changed: 163 additions & 4 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter_test/flutter_test.dart';
4+
import 'package:mockito/mockito.dart';
5+
import 'package:segment_analytics/analytics.dart';
6+
import 'package:segment_analytics/analytics_platform_interface.dart';
7+
import 'package:segment_analytics/event.dart';
8+
import 'package:segment_analytics/plugin.dart';
9+
import 'package:segment_analytics/state.dart';
10+
11+
import '../mocks/mocks.dart';
12+
import '../mocks/mocks.mocks.dart';
13+
14+
/// Simulates the BUGGY PluginIdfa behavior (before PR #198): fires off async
15+
/// IDFA fetch in constructor without overriding execute() to await it.
16+
class BuggyIdfaPlugin extends Plugin {
17+
final Completer<void> _fetchCompleter;
18+
19+
BuggyIdfaPlugin(this._fetchCompleter) : super(PluginType.enrichment) {
20+
_simulateIdfaFetch();
21+
}
22+
23+
Future<void> _simulateIdfaFetch() async {
24+
await _fetchCompleter.future;
25+
final context = await analytics?.state.context.state;
26+
if (context != null) {
27+
context.device.advertisingId = 'test-advertising-id';
28+
context.device.adTrackingEnabled = true;
29+
analytics?.state.context.setState(context);
30+
}
31+
}
32+
}
33+
34+
/// Simulates the FIXED PluginIdfa behavior (PR #198): stores the future and
35+
/// awaits it in execute(), blocking events until IDFA data is available.
36+
class FixedIdfaPlugin extends Plugin {
37+
final Completer<void> _fetchCompleter;
38+
late final Future<void> _idfaFuture;
39+
40+
FixedIdfaPlugin(this._fetchCompleter) : super(PluginType.enrichment) {
41+
_idfaFuture = _simulateIdfaFetch();
42+
}
43+
44+
Future<void> _simulateIdfaFetch() async {
45+
await _fetchCompleter.future;
46+
final context = await analytics?.state.context.state;
47+
if (context != null) {
48+
context.device.advertisingId = 'test-advertising-id';
49+
context.device.adTrackingEnabled = true;
50+
analytics?.state.context.setState(context);
51+
}
52+
}
53+
54+
@override
55+
Future<RawEvent?> execute(RawEvent event) async {
56+
await _idfaFuture;
57+
return event;
58+
}
59+
}
60+
61+
void main() {
62+
TestWidgetsFlutterBinding.ensureInitialized();
63+
64+
const writeKey = '123';
65+
final batch = [
66+
TrackEvent("Event 1"),
67+
TrackEvent("Event 2"),
68+
TrackEvent("Event 3"),
69+
];
70+
71+
group('IDFA plugin - advertisingId on initial events', () {
72+
late Analytics analytics;
73+
late MockHTTPClient httpClient;
74+
75+
setUp(() async {
76+
AnalyticsPlatform.instance = MockPlatform();
77+
httpClient = Mocks.httpClient();
78+
when(httpClient.settingsFor(writeKey))
79+
.thenAnswer((_) => Future.value(SegmentAPISettings({})));
80+
when(httpClient.startBatchUpload(writeKey, batch))
81+
.thenAnswer((_) => Future.value(true));
82+
analytics = Analytics(
83+
Configuration(writeKey,
84+
trackApplicationLifecycleEvents: false,
85+
token: "test-token"),
86+
Mocks.store(),
87+
httpClient: (_) => httpClient);
88+
await analytics.init();
89+
});
90+
91+
test('regression: without execute() override, events pass through before '
92+
'IDFA is ready', () async {
93+
// Demonstrates the bug from before PR #198: an enrichment plugin that
94+
// does async work in constructor but doesn't override execute() lets
95+
// events through immediately. This causes context.device.advertisingId
96+
// to be null when events are serialized to the queue.
97+
final fetchCompleter = Completer<void>();
98+
final plugin = BuggyIdfaPlugin(fetchCompleter);
99+
analytics.addPlugin(plugin);
100+
101+
bool executeReturned = false;
102+
plugin.execute(TrackEvent("Application Opened")).then((_) {
103+
executeReturned = true;
104+
});
105+
106+
await Future<void>.delayed(Duration.zero);
107+
108+
// Confirms the buggy behavior: execute() returned without waiting
109+
expect(executeReturned, isTrue,
110+
reason: 'Without execute() override, events pass through immediately');
111+
112+
fetchCompleter.complete();
113+
});
114+
115+
test('fix: with execute() override, events are blocked until IDFA resolves',
116+
() async {
117+
// Validates the fix from PR #198: by overriding execute() to await the
118+
// IDFA future, no event can pass through the enrichment phase until
119+
// advertisingId is populated in context.
120+
final fetchCompleter = Completer<void>();
121+
final plugin = FixedIdfaPlugin(fetchCompleter);
122+
analytics.addPlugin(plugin);
123+
124+
bool executeReturned = false;
125+
final executeFuture =
126+
plugin.execute(TrackEvent("Application Opened")).then((result) {
127+
executeReturned = true;
128+
return result;
129+
});
130+
131+
await Future<void>.delayed(Duration.zero);
132+
133+
// execute() is still blocked — waiting for IDFA
134+
expect(executeReturned, isFalse,
135+
reason: 'execute() must block until IDFA data is available');
136+
137+
// Simulate native ATTrackingManager callback
138+
fetchCompleter.complete();
139+
await executeFuture;
140+
141+
// advertisingId is now set before event passes through
142+
expect(executeReturned, isTrue);
143+
final context = await analytics.state.context.state;
144+
expect(context!.device.advertisingId, equals('test-advertising-id'));
145+
});
146+
});
147+
}

packages/plugins/plugin_idfa/ios/Classes/PluginIdfaPlugin.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ public class PluginIdfaPlugin: NSObject, FlutterPlugin, NativeIdfaApi {
77
func getTrackingAuthorizationStatus(completion: @escaping (Result<NativeIdfaData, Error>) -> Void) {
88
if #available(iOS 14, *) {
99
ATTrackingManager.requestTrackingAuthorization { status in
10-
let idfa = status == .authorized ? ASIdentifierManager.shared().advertisingIdentifier.uuidString : self.fallbackValue
10+
let idfa = status == .authorized ? ASIdentifierManager.shared().advertisingIdentifier.uuidString : "00000000-0000-0000-0000-000000000000"
1111

1212
completion(.success(NativeIdfaData(
1313
adTrackingEnabled: status == .authorized,
14-
advertisingId: idfa!,
14+
advertisingId: idfa,
1515
trackingStatus: status == .authorized ? TrackingStatus.authorized : status == .denied ? TrackingStatus.denied : status == .notDetermined ? TrackingStatus.notDetermined : status == .restricted ? TrackingStatus.restricted : TrackingStatus.unknown //self.statusToString(status)
1616
)));
1717
}

packages/plugins/plugin_idfa/lib/plugin_idfa.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:io';
22

3+
import 'package:segment_analytics/event.dart';
34
import 'package:segment_analytics/plugin.dart';
45
import 'package:segment_analytics_plugin_idfa/native_idfa.dart';
56

@@ -15,6 +16,8 @@ class IdfaData {
1516
}
1617

1718
class PluginIdfa extends Plugin {
19+
Future<IdfaData>? _trackingStatusFuture;
20+
1821
PluginIdfa({bool shouldAskPermission = true}) : super(PluginType.enrichment) {
1922
if (kIsWeb) {
2023
return;
@@ -23,15 +26,24 @@ class PluginIdfa extends Plugin {
2326
return;
2427
}
2528
if (shouldAskPermission) {
26-
getTrackingStatus();
29+
_trackingStatusFuture = getTrackingStatus();
2730
}
2831
}
2932

33+
@override
34+
Future<RawEvent?> execute(RawEvent event) async {
35+
// Wait for the initial IDFA fetch to complete before allowing events through,
36+
// so that Application Installed / Application Opened are stamped with IDFA data.
37+
await _trackingStatusFuture;
38+
return event;
39+
}
40+
3041
/// `requestTrackingPermission()` will prompt the user for
3142
/// tracking permission and returns a promise you can use to
3243
/// make additional tracking decisions based on the user response
3344
Future<bool> requestTrackingPermission() async {
34-
final idfaData = await getTrackingStatus();
45+
_trackingStatusFuture = getTrackingStatus();
46+
final idfaData = await _trackingStatusFuture!;
3547
return idfaData.adTrackingEnabled;
3648
}
3749

0 commit comments

Comments
 (0)