Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Link holdback] Add ahead lookup call for exposure logging. #10565

Open
wants to merge 32 commits into
base: master
Choose a base branch
from

Conversation

carlosmuvi-stripe
Copy link
Collaborator

@carlosmuvi-stripe carlosmuvi-stripe commented Apr 3, 2025

Summary

  • As part of the exposure logging, we'll be calling a no-cache, no-backend-logging lookup if we can retrieve an email when retrieving the session. On a follow-up PR, this will be put behind a killswitch so we can turn it off after the experiment is done.
  • The lookup call is used to compute one of the key dimensions of the holdback, wether or not that email corresponds to an existing link customer.
  • We're replicating the approach followed on web. For more context, see this section of the doc.

Motivation

Testing

  • Added tests
  • Modified tests
  • Manually verified

Screenshots

Before After
before screenshot after screenshot

Changelog

Copy link
Contributor

github-actions bot commented Apr 3, 2025

Diffuse output:

OLD: paymentsheet-example-release-master.apk (signature: V1, V2)
NEW: paymentsheet-example-release-pr.apk (signature: V1, V2)

          │           compressed           │          uncompressed          
          ├───────────┬───────────┬────────┼──────────┬──────────┬──────────
 APK      │ old       │ new       │ diff   │ old      │ new      │ diff     
──────────┼───────────┼───────────┼────────┼──────────┼──────────┼──────────
      dex │   4.1 MiB │   4.1 MiB │ +724 B │  9.1 MiB │  9.1 MiB │ +3.5 KiB 
     arsc │   2.4 MiB │   2.4 MiB │    0 B │  2.4 MiB │  2.4 MiB │      0 B 
 manifest │   5.3 KiB │   5.3 KiB │    0 B │ 26.9 KiB │ 26.9 KiB │      0 B 
      res │ 910.6 KiB │ 910.6 KiB │    0 B │  1.4 MiB │  1.4 MiB │      0 B 
   native │   2.6 MiB │   2.6 MiB │    0 B │    6 MiB │    6 MiB │      0 B 
    asset │   1.6 MiB │   1.6 MiB │  +58 B │  1.6 MiB │  1.6 MiB │    +58 B 
    other │   1.4 MiB │   1.4 MiB │   -7 B │  1.6 MiB │  1.6 MiB │      0 B 
──────────┼───────────┼───────────┼────────┼──────────┼──────────┼──────────
    total │    13 MiB │    13 MiB │ +775 B │ 22.3 MiB │ 22.3 MiB │ +3.6 KiB 

 DEX     │ old   │ new   │ diff                
─────────┼───────┼───────┼─────────────────────
   files │     1 │     1 │   0                 
 strings │ 43099 │ 43110 │ +11 (+4072 -4061)   
   types │ 15486 │ 15490 │  +4 (+4061 -4057)   
 classes │ 13075 │ 13078 │  +3 (+3278 -3275)   
 methods │ 63246 │ 63255 │  +9 (+24270 -24261) 
  fields │ 42136 │ 42153 │ +17 (+19346 -19329) 

 ARSC    │ old  │ new  │ diff 
─────────┼──────┼──────┼──────
 configs │  243 │  243 │  0   
 entries │ 6291 │ 6291 │  0
APK
    compressed     │     uncompressed     │                                
──────────┬────────┼───────────┬──────────┤                                
 size     │ diff   │ size      │ diff     │ path                           
──────────┼────────┼───────────┼──────────┼────────────────────────────────
  4.1 MiB │ +724 B │   9.1 MiB │ +3.5 KiB │ ∆ classes.dex                  
    127 B │ +127 B │       5 B │     +5 B │ + META-INF/services/ba.x       
    127 B │ +127 B │       5 B │     +5 B │ + META-INF/services/ca.a       
          │ -127 B │           │     -5 B │ - META-INF/services/aa.x       
          │ -127 B │           │     -5 B │ - META-INF/services/ba.a       
  8.2 KiB │  +67 B │     8 KiB │    +67 B │ ∆ assets/dexopt/baseline.prof  
    1 KiB │   -9 B │     927 B │     -9 B │ ∆ assets/dexopt/baseline.profm 
 54.1 KiB │   -5 B │ 120.2 KiB │      0 B │ ∆ META-INF/CERT.SF             
  1.2 KiB │   -1 B │   1.2 KiB │      0 B │ ∆ META-INF/CERT.RSA            
 50.7 KiB │   -1 B │ 120.1 KiB │      0 B │ ∆ META-INF/MANIFEST.MF         
──────────┼────────┼───────────┼──────────┼────────────────────────────────
  4.2 MiB │ +775 B │   9.4 MiB │ +3.6 KiB │ (total)
DEX
STRINGS:

   old   │ new   │ diff              
  ───────┼───────┼───────────────────
   43099 │ 43110 │ +11 (+4072 -4061) 
  
  + LA7/q;
  + LA7/r;
  + LA8/b;
  + LA8/c;
  + LB5/b;
  + LB5/c;
  + LB5/d;
  + LB5/e;
  + LB6/A;
  + LB6/B;
  + LB6/C;
  + LB6/D;
  + LB6/E;
  + LB6/F;
  + LB6/G;
  + LB6/H;
  + LB6/I;
  + LB6/J;
  + LB6/K;
  + LB6/L;
  + LB6/M;
  + LB6/N;
  + LB6/O;
  + LB6/P;
  + LB6/Q;
  + LB6/S;
  + LB6/T;
  + LB6/U;
  + LB6/V;
  + LB6/W;
  + LB6/X;
  + LB6/Y;
  + LB6/Z;
  + LB6/a0;
  + LB6/b0;
  + LB6/c0;
  + LB6/d0;
  + LB6/e0;
  + LB6/e;
  + LB6/f0;
  + LB6/f;
  + LB6/g0;
  + LB6/g;
  + LB6/h0;
  + LB6/h;
  + LB6/i0;
  + LB6/i;
  + LB6/j0;
  + LB6/j;
  + LB6/k;
  + LB6/l;
  + LB6/m;
  + LB6/n;
  + LB6/o;
  + LB6/p;
  + LB6/q;
  + LB6/r;
  + LB6/s;
  + LB6/t;
  + LB6/u;
  + LB6/v;
  + LB6/w;
  + LB6/x;
  + LB6/y;
  + LB6/z;
  + LB7/h;
  + LB7/i;
  + LB7/j;
  + LB7/k;
  + LB7/l;
  + LB7/m;
  + LB7/n;
  + LB7/o;
  + LB7/p;
  + LB9/A0;
  + LB9/A;
  + LB9/B;
  + LB9/C;
  + LB9/D;
  + LB9/E;
  + LB9/F;
  + LB9/G;
  + LB9/H;
  + LB9/I;
  + LB9/J;
  + LB9/K;
  + LB9/L;
  + LB9/M;
  + LB9/N;
  + LB9/O;
  + LB9/P;
  + LB9/Q;
  + LB9/S;
  + LB9/T;
  + LB9/U;
  + LB9/V;
  + LB9/W;
  + LB9/X;
  + LB9/Y;
  + LB9/Z;
  + LB9/a0;
  + LB9/b0;
  + LB9/c0;
  + LB9/d0;
  + LB9/e0;
  + LB9/f0;
  + LB9/g0;
  + LB9/h0;
  + LB9/i0;
  + LB9/j0;
  + LB9/j;
  + LB9/k0;
  + LB9/k;
  + LB9/l0;
  + LB9/l;
  + LB9/m0;
  + LB9/m;
  + LB9/n0;
  + LB9/n;
  + LB9/o0;
  + LB9/o;
  + LB9/p0;
  + LB9/p;
  + LB9/q0;
  + LB9/q;
  + LB9/r0;
  + LB9/r;
  + LB9/s0;
  + LB9/s;
  + LB9/t0;
  + LB9/t;
  + LB9/u0;
  + LB9/u;
  + LB9/v0;
  + LB9/v;
  + LB9/w0;
  + LB9/w;
  + LB9/x0;
  + LB9/x;
  + LB9/y0;
  + LB9/y;
  + LB9/z0;
  + LB9/z;
  + LBb/e;
  + LBb/f;
  + LBb/g;
  + LBb/h;
  + LC8/k;
  + LC8/l;
  + LC8/m;
  + LC8/n;
  + LC8/o;
  + LC8/p;
  + LC8/q;
  + LC8/r;
  + LC8/s;
  + LC8/t;
  + LC8/u;
  + LC8/v;
  + LC8/w;
  + LC8/x;
  + LC8/y;
  + LC9/b;
  + LC9/c;
  + LC9/d;
  + LC9/e;
  + LC9/f;
  + LC9/g;
  + LC9/h;
  + LC9/i;
  + LCa/b;
  + LCa/c;
  + LCa/d;
  + LCa/e;
  + LCa/f;
  + LCb/a;
  + LCb/b;
  + LCb/c;
  + LCb/d;
  + LD5/D;
  + LD5/E;
  + LD5/F;
  + LD5/G;
  + LD5/H;
  + LD5/I;
  + LD5/J;
  + LD5/K;
  + LD5/L;
  + LD5/M;
  + LD5/N;
  + LD5/O;
  + LD5/P;
  + LD5/Q;
  + LD5/S;
  + LD5/T;
  + LD5/U;
  + LD6/I;
  + LD6/J;
  + LD6/K;
  + LD6/L;
  + LD6/M;
  + LD6/N;
  + LD6/O;
  + LD6/P;
  + LD6/Q;
  + LD6/S;
  + LD6/T;
  + LD6/U;
  + LD6/V;
  + LD6/W;
  + LD6/X;
  + LD6/Y;
  + LD6/Z;
  + LD6/a0;
  + LD6/b0;
  + LD6/c0;
  + LD6/d0;
  + LD6/e0;
  + LD6/f0;
  + LD8/h;
  + LD8/i;
  + LD8/j;
  + LE5/A;
  + LE5/B;
  + LE5/C;
  + LE5/b;
  + LE5/c;
  + LE5/d;
  + LE5/e;
  + LE5/f;
  + LE5/g;
  + LE5/h;
  + LE5/i;
  + LE5/j;
  + LE5/k;
  + LE5/l;
  + LE5/m;
  + LE5/n;
  + LE5/o;
  + LE5/p;
  + LE5/q;
  + LE5/r;
  + LE5/s;
  + LE5/t;
  + LE5/u;
  + LE5/v;
  + LE5/w;
  + LE5/x;
  + LE5/y;
  + LE5/z;
  + LE6/A;
  + LE6/B;
  + LE6/C;
  + LE6/D;
  + LE6/E;
  + LE6/F;
  + LE6/G;
  + LE6/H;
  + LE6/g;
  + LE6/h;
  + LE6/i;
  + LE6/j;
  + LE6/k;
  + LE6/l;
  + LE6/m;
  + LE6/n;
  + LE6/o;
  + LE6/p;
  + LE6/q;
  + LE6/r;
  + LE6/s;
  + LE6/t;
  + LE6/u;
  + LE6/v;
  + LE6/w;
  + LE6/x;
  + LE6/y;
  + LE6/z;
  + LE7/g;
  + LE7/h;
  + LE7/i;
  + LE7/j;
  + LE7/k;
  + LE7/l;
  + LE7/m;
  + LE7/n;
  + LE7/o;
  + LE7/p;
  + LE7/q;
  + LE7/r;
  + LE7/s;
  + LE7/t;
  + LE7/u;
  + LE7/v;
  + LE9/b;
  + LE9/c;
  + LE9/d;
  + LE9/e;
  + LE9/f;
  + LE9/g;
  + LE9/h;
  + LE9/i;
  + LEa/b;
  + LEa/c;
  + LEa/d;
  + LEa/e;
  + LEb/a;
  + LF5/a;
  + LF7/c;
  + LF7/d;
  + LF7/e;
  + LF7/f;
  + LF8/b;
  + LF8/c;
  + LF8/d;
  + LF8/e;
  + LF8/f;
  + LF8/g;
  + LF8/h;
  + LG6/h;
  + LH6/b;
  + LH6/c;
  + LH6/d;
  + LH6/e;
  + LH6/f;
  + LH6/g;
  + LH8/A;
  + LH8/B;
  + LH8/C;
  + LH8/D;
  + LH8/E;
  + LH8/F;
  + LH8/G;
  + LH8/H;
  + LH8/I;
  + LH8/J;
  + LH8/K;
  + LH8/L;
  + LH8/M;
  + LH8/N;
  + LH8/O;
  + LH8/P;
  + LH8/Q;
  + LH8/S;
  + LH8/T;
  + LH8/U;
  + LH8/V;
  + LH8/W;
  + LH8/X;
  + LH8/Y;
  + LH8/Z;
  + LH8/a0;
  + LH8/b0;
  + LH8/c0;
  + LH8/d0;
  + LH8/l;
  + LH8/m;
  + LH8/n;
  + LH8/o;
  + LH8/p;
  + LH8/q;
  + LH8/r;
  + LH8/s;
  + LH8/t;
  + LH8/u;
  + LH8/v;
  + LH8/w;
  + LH8/x;
  + LH8/y;
  + LH8/z;
  + LH9/b;
  + LH9/c;
  + LH9/d;
  + LH9/e;
  + LH9/f;
  + LH9/g;
  + LH9/h;
  + LH9/i;
  + LH9/j;
  + LH9/k;
  + LH9/l;
  + LHa/b;
  + LHa/c;
  + LI5/A0;
  + LI5/A;
  + LI5/B0;
  + LI5/B;
  + LI5/C0;
  + LI5/C;
  + LI5/D0;
  + LI5/D;
  + LI5/E0;
  + LI5/E
...✂

Base automatically changed from carlosmuvi/add-v2-reporting-to-mpe to master April 4, 2025 01:46
val experimentsData = requireNotNull(
elementsSession.experimentsData
) { "Experiments data required to log exposures" }
val holdbackOn = elementsSession.linkSettings?.linkGlobalHoldbackOn == true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, we should instead look at elementsSession.experimentsData.experimentAssignments["link_global_holdback"] to get the group. In testmode, this key will not be present, so we treat that as though it were the control group. Confirming here though: https://stripe.slack.com/archives/C05RD8RBF5L/p1743774491875609?thread_ts=1743189375.360319&cid=C05RD8RBF5L

With this, I don't think we need the linkGlobalHoldbackOn field on link_settings for anything?

Copy link
Collaborator Author

@carlosmuvi-stripe carlosmuvi-stripe Apr 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated it here - 8a30e76 (including removing linkGlobalHoldbackOn)

group = group,
name = "link_global_holdback",
dimensions = mapOf(
"integration_type" to "mpe",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we doing ios / android integration types? Or grouping all of MPE together?

Suggested change
"integration_type" to "mpe",
"integration_type" to "mpe_android",

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…onsumer-for-exposure

# Conflicts:
#	paymentsheet/src/main/java/com/stripe/android/common/analytics/experiment/LogLinkGlobalHoldbackExposure.kt
#	paymentsheet/src/main/java/com/stripe/android/common/analytics/experiment/LoggableExperiment.kt
#	paymentsheet/src/test/java/com/stripe/android/common/analytics/experiment/LogLinkGlobalHoldbackExposureTest.kt
#	paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporterTest.kt
Comment on lines 173 to 177
val avoidConsumerLoggingParams: Map<String, Boolean> = if (doNotLogConsumerFunnelEvent) {
mapOf("TEMPORARY_AND_DANGEROUS__do_not_log_consumer_funnel_event" to true)
} else {
emptyMap()
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is unfortunate, but should go away after the holdback.


internal class LogLinkGlobalHoldbackExposure @Inject constructor(
internal interface LogLinkGlobalHoldbackExposure {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interface to facilitate adding a Fake.

@carlosmuvi-stripe carlosmuvi-stripe marked this pull request as ready for review April 7, 2025 18:26
@carlosmuvi-stripe carlosmuvi-stripe requested review from a team as code owners April 7, 2025 18:26
@@ -45,6 +45,7 @@ interface ConsumersApiService {
suspend fun lookupConsumerSession(
email: String,
requestSurface: String,
doNotLogConsumerFunnelEvent: Boolean = false,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Should we remove the default param here, so that callers are forced to think about which values to provide?

Comment on lines 90 to 97
.map { it.exists }
.fold(
onSuccess = { it },
onFailure = {
logger.error("Failed to check if user is returning", it)
false
}
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: No need to fold.

Suggested change
.map { it.exists }
.fold(
onSuccess = { it },
onFailure = {
logger.error("Failed to check if user is returning", it)
false
}
)
.map { it.exists }
.onFailure {
logger.error("Failed to check if user is returning", it)
false
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onFailure does not allow error recovery, but getOrElse does the trick!


val customerEmail = state.getEmail()

val isReturningUser: Boolean = customerEmail?.let { isReturningUser(email = it) } == true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We can use to compiler to our advantage and clean this up a bit.

Suggested change
val isReturningUser: Boolean = customerEmail?.let { isReturningUser(email = it) } == true
val isReturningUser = customerEmail != null && isReturningUser(customerEmail)

Comment on lines 101 to 111
paymentMethodMetadata.linkState?.configuration?.customerInfo?.email
?: config.defaultBillingDetails?.email
?: customer?.let {
customerRepository.retrieveCustomer(
CustomerRepository.CustomerInfo(
id = it.id,
ephemeralKeySecret = it.ephemeralKeySecret,
customerSessionClientSecret = it.customerSessionClientSecret
)
)
}?.email
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have similar code here to determine the actual customer email.

Can we extract that into a method and then use it in both the PaymentElementLoader and in here?

@@ -27,6 +27,15 @@ internal interface LinkRepository {
email: String,
): Result<ConsumerSessionLookup>

/**
* Performs a lookup of a consumer session without triggering any
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Performs a lookup of a consumer session without triggering any
* Performs a lookup of a consumer session without triggering any

val exposureCall = eventReporter.experimentExposureCalls.awaitItem()
assertTrue(exposureCall.experiment is LoggableExperiment.LinkGlobalHoldback)
assertEquals(exposureCall.experiment.group, "holdback")
assertTrue((exposureCall.experiment.isReturningLinkConsumer))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

((…))

Let’s use Truth’s assertThat() instead.

val exposureCall = eventReporter.experimentExposureCalls.awaitItem()
assertTrue(exposureCall.experiment is LoggableExperiment.LinkGlobalHoldback)
assertEquals(exposureCall.experiment.group, "holdback")
assertFalse((exposureCall.experiment.isReturningLinkConsumer))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

((…))

Let’s use Truth’s assertThat() instead.

@carlosmuvi-stripe
Copy link
Collaborator Author

@tillh-stripe addressed all feedback here!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants