Skip to content

Commit 2a9156b

Browse files
committed
Call back from the firebase transport runtime into the event-emitter if there is an updated pseudonymous ID
This then allows the emitter to store the ID and attach it to future events. Note: Ideally we would not even attach the ID to the event and rather resolve it directly before the http-call. Not sure why this was not done. But this is not really a big issue as long as the ids are very long living. Then we will we only end up with a small amount of events with stale IDs I thought about how to best implement the callback. - We can unfortunately not just use a regular Consumer class as the callback has to be persistable. - Since the firebase transport runtime does not use Hilt's app-wide shared components I don't think we can inject the callback using dagger - We could use Android functionality like e.g. a BroadcastReceiver, but this is pretty heavy handed Internal b/490114654
1 parent 0f997c2 commit 2a9156b

File tree

20 files changed

+280
-38
lines changed

20 files changed

+280
-38
lines changed

transport/transport-api/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
22

3+
- [feature] Support callback when the pseudonymous id was updated
4+
35
# 4.1.0
46

57
- [feature] Support multiple encrypted experiment IDs.

transport/transport-api/src/main/java/com/google/android/datatransport/EventContext.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ public abstract class EventContext {
3636
@SuppressWarnings("mutable")
3737
public abstract List<byte[]> getExperimentIdsEncryptedList();
3838

39+
@Nullable
40+
public abstract String getPseudonymousIdUpdateReceiverClassName();
41+
3942
public static Builder builder() {
4043
return new AutoValue_EventContext.Builder();
4144
}
@@ -54,6 +57,9 @@ public abstract static class Builder {
5457
@NonNull
5558
public abstract Builder setExperimentIdsEncryptedList(List<byte[]> value);
5659

60+
@NonNull
61+
public abstract Builder setPseudonymousIdUpdateReceiverClassName(String value);
62+
5763
@NonNull
5864
public abstract EventContext build();
5965
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.android.datatransport;
16+
17+
import android.content.Context;
18+
import androidx.annotation.NonNull;
19+
20+
/**
21+
* This {@link PseudonymousIdUpdateReceiver} will be receiving pseudonymous id updates from the
22+
* transport.
23+
*
24+
* <p>The constructor needs to take a single {@link Context} parameter.
25+
*
26+
* <p>Make sure that the constructor and the {@link #setUpdatedPseudonymousId} method are not
27+
* removed by Proguard (add @Keep annotation if needed).
28+
*/
29+
public interface PseudonymousIdUpdateReceiver {
30+
/** Sets the updated pseudonymous id. */
31+
void setUpdatedPseudonymousId(@NonNull String updatedPseudonymousId);
32+
}

transport/transport-backend-cct/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
22

3+
- [feature] Support callback when the pseudonymous id was updated
4+
35
# 4.1.0
46

57
- [feature] Support multiple encrypted experiment IDs.

transport/transport-backend-cct/src/main/java/com/google/android/datatransport/cct/CctTransportBackend.java

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,27 @@ private BatchedLogRequest getRequestBody(BackendRequest backendRequest) {
342342
return BatchedLogRequest.create(batchedRequests);
343343
}
344344

345+
private String getPseudonymousId(HttpURLConnection connection) {
346+
String setCookieHeader = connection.getHeaderField("Set-Cookie");
347+
if (setCookieHeader != null) {
348+
String[] cookieHeaderParts = setCookieHeader.split(";");
349+
350+
for (String cookieHeaderPart : cookieHeaderParts) {
351+
String[] cookieKeyValue = cookieHeaderPart.trim().split("=", 2);
352+
if (cookieKeyValue.length != 2) {
353+
continue;
354+
}
355+
String cookieKey = cookieKeyValue[0];
356+
String cookieValue = cookieKeyValue[1];
357+
358+
if (cookieKey.equals("NID")) {
359+
return cookieValue;
360+
}
361+
}
362+
}
363+
return null;
364+
}
365+
345366
private HttpResponse doSend(HttpRequest request) throws IOException {
346367
Logging.i(LOG_TAG, "Making request to: %s", request.url);
347368
HttpURLConnection connection = (HttpURLConnection) request.url.openConnection();
@@ -373,10 +394,10 @@ private HttpResponse doSend(HttpRequest request) throws IOException {
373394
request.requestBody, new BufferedWriter(new OutputStreamWriter(outputStream)));
374395
} catch (ConnectException | UnknownHostException e) {
375396
Logging.e(LOG_TAG, "Couldn't open connection, returning with 500", e);
376-
return new HttpResponse(500, null, 0);
397+
return new HttpResponse(500, null, 0, null);
377398
} catch (EncodingException | IOException e) {
378399
Logging.e(LOG_TAG, "Couldn't encode request, returning with 400", e);
379-
return new HttpResponse(400, null, 0);
400+
return new HttpResponse(400, null, 0, null);
380401
}
381402

382403
int responseCode = connection.getResponseCode();
@@ -386,10 +407,10 @@ private HttpResponse doSend(HttpRequest request) throws IOException {
386407

387408
if (responseCode == 302 || responseCode == 301 || responseCode == 307) {
388409
String redirect = connection.getHeaderField("Location");
389-
return new HttpResponse(responseCode, new URL(redirect), 0);
410+
return new HttpResponse(responseCode, new URL(redirect), 0, null);
390411
}
391412
if (responseCode != 200) {
392-
return new HttpResponse(responseCode, null, 0);
413+
return new HttpResponse(responseCode, null, 0, null);
393414
}
394415

395416
try (InputStream connStream = connection.getInputStream();
@@ -398,7 +419,7 @@ private HttpResponse doSend(HttpRequest request) throws IOException {
398419
long nextRequestMillis =
399420
LogResponse.fromJson(new BufferedReader(new InputStreamReader(inputStream)))
400421
.getNextRequestWaitMillis();
401-
return new HttpResponse(responseCode, null, nextRequestMillis);
422+
return new HttpResponse(responseCode, null, nextRequestMillis, getPseudonymousId(connection));
402423
}
403424
}
404425

@@ -469,7 +490,7 @@ public BackendResponse send(BackendRequest request) {
469490
});
470491

471492
if (response.code == 200) {
472-
return BackendResponse.ok(response.nextRequestMillis);
493+
return BackendResponse.ok(response.nextRequestMillis, response.updatedPseudonymousId);
473494
} else if (response.code >= 500 || response.code == 404) {
474495
return BackendResponse.transientError();
475496
} else if (response.code == 400) {
@@ -493,11 +514,17 @@ static final class HttpResponse {
493514
final int code;
494515
@Nullable final URL redirectUrl;
495516
final long nextRequestMillis;
517+
@Nullable final String updatedPseudonymousId;
496518

497-
HttpResponse(int code, @Nullable URL redirectUrl, long nextRequestMillis) {
519+
HttpResponse(
520+
int code,
521+
@Nullable URL redirectUrl,
522+
long nextRequestMillis,
523+
@Nullable String updatedPseudonymousId) {
498524
this.code = code;
499525
this.redirectUrl = redirectUrl;
500526
this.nextRequestMillis = nextRequestMillis;
527+
this.updatedPseudonymousId = updatedPseudonymousId;
501528
}
502529
}
503530

transport/transport-backend-cct/src/test/java/com/google/android/datatransport/cct/CctTransportBackendTest.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ public void testCCTSuccessLoggingRequest() {
292292
JSON_PAYLOAD_ESCAPED)))
293293
.withoutHeader("Cookie"));
294294

295-
assertEquals(BackendResponse.ok(3), response);
295+
assertEquals(BackendResponse.ok(3, null), response);
296296
}
297297

298298
@Test
@@ -391,7 +391,7 @@ public void testCCTSuccessLoggingRequestWithoutEncryptedList() {
391391
JSON_PAYLOAD_ESCAPED)))
392392
.withoutHeader("Cookie"));
393393

394-
assertEquals(BackendResponse.ok(3), response);
394+
assertEquals(BackendResponse.ok(3, null), response);
395395
}
396396

397397
@Test
@@ -490,7 +490,7 @@ public void testCCTSuccessLoggingRequestWithJustEncryptedList() {
490490
JSON_PAYLOAD_ESCAPED)))
491491
.withoutHeader("Cookie"));
492492

493-
assertEquals(BackendResponse.ok(3), response);
493+
assertEquals(BackendResponse.ok(3, null), response);
494494
}
495495

496496
@Test
@@ -536,7 +536,7 @@ public void testCCTContainsRightAndroidClientInfo() {
536536
String.format(
537537
"$[?(@.logRequest[0].clientInfo.androidClientInfo.mccMnc == \"\")]"))));
538538

539-
assertEquals(BackendResponse.ok(3), response);
539+
assertEquals(BackendResponse.ok(3, null), response);
540540
}
541541

542542
@Test
@@ -570,7 +570,7 @@ public void testCCTContainsRightApplicationBuild() throws NameNotFoundException
570570
ApplicationProvider.getApplicationContext().getPackageName(),
571571
/* flags= */ 0)
572572
.versionCode))));
573-
assertEquals(BackendResponse.ok(3), response);
573+
assertEquals(BackendResponse.ok(3, null), response);
574574
}
575575

576576
@Test
@@ -806,7 +806,7 @@ public void send_whenBackendRedirects_shouldCorrectlyFollowTheRedirectViaPost()
806806
postRequestedFor(urlEqualTo("/api/hello"))
807807
.withHeader("Content-Type", equalTo("application/json")));
808808

809-
assertEquals(BackendResponse.ok(3), response);
809+
assertEquals(BackendResponse.ok(3, null), response);
810810
}
811811

812812
@Test
@@ -836,7 +836,7 @@ public void send_whenBackendRedirectswith307_shouldCorrectlyFollowTheRedirectVia
836836
postRequestedFor(urlEqualTo("/api/hello"))
837837
.withHeader("Content-Type", equalTo("application/json")));
838838

839-
assertEquals(BackendResponse.ok(3), response);
839+
assertEquals(BackendResponse.ok(3, null), response);
840840
}
841841

842842
@Test
@@ -895,7 +895,7 @@ public void send_CompressedResponseIsUncompressed() throws IOException {
895895
.withHeader("Content-Type", equalTo("application/json"))
896896
.withHeader("Content-Encoding", equalTo("gzip")));
897897

898-
assertEquals(BackendResponse.ok(3), response);
898+
assertEquals(BackendResponse.ok(3, null), response);
899899
}
900900

901901
@Test
@@ -952,7 +952,7 @@ public void send_whenLogSourceIsSetByName_shouldSetItToProperField() throws IOEx
952952
matchingJsonPath(
953953
String.format("$[?(@.logRequest[1].logSourceName == \"%s\")]", TEST_NAME))));
954954

955-
assertEquals(BackendResponse.ok(3), response);
955+
assertEquals(BackendResponse.ok(3, null), response);
956956
}
957957

958958
@Test
@@ -1006,7 +1006,7 @@ public void send_withEventsOfUnsupportedEncoding_shouldBeSkipped() throws IOExce
10061006
String.format("$[?(@.logRequest[1].logSourceName == \"%s\")]", TEST_NAME)))
10071007
.withRequestBody(matchingJsonPath("$[?(@.logRequest[1].logEvent.size() == 1)]")));
10081008

1009-
assertEquals(BackendResponse.ok(3), response);
1009+
assertEquals(BackendResponse.ok(3, null), response);
10101010
}
10111011

10121012
@Test

transport/transport-runtime/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Unreleased
22

33
- [changed] Bumped internal dependencies.
4+
- [feature] Support callback when the pseudonymous id was updated
45

56
# 4.1.0
67

transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/EventInternal.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ public byte[] getPayload() {
5858
@Nullable
5959
public abstract List<byte[]> getExperimentIdsEncryptedList();
6060

61+
@Nullable
62+
public abstract String getPseudonymousIdUpdateReceiverClassName();
63+
6164
public final Map<String, String> getMetadata() {
6265
return Collections.unmodifiableMap(getAutoMetadata());
6366
}
@@ -91,6 +94,7 @@ public Builder toBuilder() {
9194
.setExperimentIdsClear(getExperimentIdsClear())
9295
.setExperimentIdsEncrypted(getExperimentIdsEncrypted())
9396
.setExperimentIdsEncryptedList(getExperimentIdsEncryptedList())
97+
.setPseudonymousIdUpdateReceiverClassName(getPseudonymousIdUpdateReceiverClassName())
9498
.setEncodedPayload(getEncodedPayload())
9599
.setEventMillis(getEventMillis())
96100
.setUptimeMillis(getUptimeMillis())
@@ -125,6 +129,8 @@ public abstract static class Builder {
125129

126130
public abstract Builder setExperimentIdsEncryptedList(List<byte[]> value);
127131

132+
public abstract Builder setPseudonymousIdUpdateReceiverClassName(String value);
133+
128134
protected abstract Map<String, String> getAutoMetadata();
129135

130136
public final Builder addMetadata(String key, String value) {

transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/TransportRuntime.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ private EventInternal convert(SendRequest request) {
172172
if (eventContext.getPseudonymousId() != null) {
173173
builder.setPseudonymousId(eventContext.getPseudonymousId());
174174
}
175+
if (eventContext.getPseudonymousIdUpdateReceiverClassName() != null) {
176+
builder.setPseudonymousIdUpdateReceiverClassName(
177+
eventContext.getPseudonymousIdUpdateReceiverClassName());
178+
}
175179
if (eventContext.getExperimentIdsClear() != null) {
176180
builder.setExperimentIdsClear(eventContext.getExperimentIdsClear());
177181
}

transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/backends/BackendResponse.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.android.datatransport.runtime.backends;
1616

17+
import androidx.annotation.Nullable;
1718
import com.google.auto.value.AutoValue;
1819

1920
/**
@@ -36,19 +37,24 @@ public enum Status {
3637
/** Time in millis to wait before attempting another request. */
3738
public abstract long getNextRequestWaitMillis();
3839

40+
/** Updated pseudonymous id from the backend or {@code null} if there is no update. */
41+
@Nullable
42+
public abstract String getUpdatedPseudonymousId();
43+
3944
public static BackendResponse transientError() {
40-
return new AutoValue_BackendResponse(Status.TRANSIENT_ERROR, -1);
45+
return new AutoValue_BackendResponse(Status.TRANSIENT_ERROR, -1, null);
4146
}
4247

4348
public static BackendResponse fatalError() {
44-
return new AutoValue_BackendResponse(Status.FATAL_ERROR, -1);
49+
return new AutoValue_BackendResponse(Status.FATAL_ERROR, -1, null);
4550
}
4651

4752
public static BackendResponse invalidPayload() {
48-
return new AutoValue_BackendResponse(Status.INVALID_PAYLOAD, -1);
53+
return new AutoValue_BackendResponse(Status.INVALID_PAYLOAD, -1, null);
4954
}
5055

51-
public static BackendResponse ok(long nextRequestWaitMillis) {
52-
return new AutoValue_BackendResponse(Status.OK, nextRequestWaitMillis);
56+
public static BackendResponse ok(
57+
long nextRequestWaitMillis, @Nullable String updatedPseudonymousId) {
58+
return new AutoValue_BackendResponse(Status.OK, nextRequestWaitMillis, updatedPseudonymousId);
5359
}
5460
}

0 commit comments

Comments
 (0)