-
Notifications
You must be signed in to change notification settings - Fork 435
Invite a friend: referral attribution across the analytics stack #5751
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
Open
shai-almog
wants to merge
45
commits into
master
Choose a base branch
from
invite-attribution
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
45 commits
Select commit
Hold shift + click to select a range
e2b4b11
Invite attribution: core client API, catalog entry and package guard
shai-almog 1a5055e
Invite attribution: tests, and the analysis gates they have to pass
shai-almog 5b44d6b
Invite attribution: declare the build hints the link plumbing will read
shai-almog 8f64c92
Invite attribution: make an invite link open the app, on both platforms
shai-almog 76b058b
Invite attribution: read the Play Install Referrer, and drop a floor …
shai-almog 95e69cb
Invite attribution: document it in the developer guide
shai-almog 63b514b
Invite attribution: simulate the deferred path without a device
shai-almog 5c6d14f
Android: deliver a link that reaches a running activity to the router
shai-almog 47a8945
Invite attribution: address the review, and clear the PMD gate
shai-almog 3c43f5c
Invite attribution: second review round, and a domain the client neve…
shai-almog e63dfed
Invite attribution: third review round on the client
shai-almog bfe5429
Invites: six ways attribution used to be lost quietly
shai-almog 5610439
Invites: four more ways a terminal answer was reached too early
shai-almog 57fc4c3
Invites: a direct link now wins, and a pending answer is not a termin…
shai-almog 43b57ce
Android port: exclude the referrer package from the Ant build too
shai-almog 5a71e81
Invites: six answers that reached nobody, or reached the wrong conclu…
shai-almog 37706e6
Invites: an erasure that does not depend on who is registered
shai-almog 81c7aa8
Invites: five consequences of the last two rounds
shai-almog 1546075
Invites: re-attribution no longer contradicts the answer it already gave
shai-almog 7a767ca
Invites: three more that the last two rounds' fixes opened
shai-almog abfb082
Invites: provenance, abandonment, and a fragment that became part of …
shai-almog cdb508d
Invites: a direct link is its own question, and a reopened one keeps …
shai-almog 8bfb8eb
Invites: four places the state machine forgot what it already knew
shai-almog 9dbd595
Invites: the window bounds the guess, not the answer
shai-almog 8e45b87
Invites: one delivery is not one url, and a failed write is not a res…
shai-almog 9324a40
Invites: consuming the argument, and three restarts that should not h…
shai-almog e7c14df
Invites: an erasure has to reach the durable records, not just the di…
shai-almog b3667c2
Invites: the reopened marker is converted, not rebuilt
shai-almog abb2a1b
Invites: three more places a write's result was assumed
shai-almog 883f9aa
Invites: reopen with a usable window and a real device profile
shai-almog 242bad2
Invites: a failed pending write no longer loses what it was writing
shai-almog 1dcfc6b
Invites: two ways a registration said yes when the answer was no
shai-almog 9fe42d0
Invites: reconcile the cached state when a held record finally lands
shai-almog 3c16d23
Invites: getState() never answers from a cache the record contradicts
shai-almog 2afe6d2
Invites: four review findings, one of them a design decision reversed
shai-almog 3b1d4c0
Invites: an erasure that lasts, a kill switch that reaches the wire
shai-almog 0d0e067
Invites: the kill switch is about the guess, not about being deferred
shai-almog 7c75853
Invites: an erasure that survives a failed write, and three smaller h…
shai-almog e629d9e
Invites: an erasure that is checked, and an answer that can arrive to…
shai-almog 3330a86
Invites: the erasure has to take the outbox, and a refusal has to settle
shai-almog 25dffdd
Invites: App Clips replace the statistical match on iOS
shai-almog 018119b
Invites: five holes the App Clip path and the erasure left open
shai-almog 3ef1b8c
Invites: the App Clip is generated now, not just entitled
shai-almog 0132106
Invites: a transient store failure is not an answer, and the clip is …
shai-almog e18bff5
Invites: the retry could not deliver, and the app group was comma-joined
shai-almog File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
CodenameOne/src/com/codename1/analytics/invite/AppClipHandoffCallback.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| /* | ||
| * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. | ||
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | ||
| * This code is free software; you can redistribute it and/or modify it | ||
| * under the terms of the GNU General Public License version 2 only, as | ||
| * published by the Free Software Foundation. Codename One designates this | ||
| * particular file as subject to the "Classpath" exception as provided | ||
| * by Oracle in the LICENSE file that accompanied this code. | ||
| * | ||
| * This code is distributed in the hope that it will be useful, but WITHOUT | ||
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | ||
| * version 2 for more details (a copy is included in the LICENSE file that | ||
| * accompanied this code). | ||
| * | ||
| * You should have received a copy of the GNU General Public License version | ||
| * 2 along with this work; if not, write to the Free Software Foundation, | ||
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | ||
| * | ||
| * Please contact Codename One through http://www.codenameone.com/ if you | ||
| * need additional information or have any questions. | ||
| */ | ||
| package com.codename1.analytics.invite; | ||
|
|
||
| /// Receives the answer to [AppClipHandoffSource#requestHandoff]. | ||
| /// | ||
| /// Exactly one method is called, once. | ||
| public interface AppClipHandoffCallback { | ||
| /// Called with the invite code an App Clip recorded. | ||
| /// | ||
| /// #### Parameters | ||
| /// | ||
| /// - `code`: the invite code the clip received, never empty | ||
| /// | ||
| /// - `clickedSeconds`: when the link was tapped, in seconds since the | ||
| /// epoch, or 0 when the clip did not record it | ||
| void onHandoff(String code, long clickedSeconds); | ||
|
|
||
| /// Called when no clip handoff exists. This is the normal answer for | ||
| /// somebody who installed the application without ever tapping an invite | ||
| /// link, and is not an error. | ||
| /// | ||
| /// #### Parameters | ||
| /// | ||
| /// - `reason`: one of the `REASON_` constants on [Invites] | ||
| void onUnavailable(String reason); | ||
| } |
65 changes: 65 additions & 0 deletions
65
CodenameOne/src/com/codename1/analytics/invite/AppClipHandoffSource.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| /* | ||
| * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. | ||
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | ||
| * This code is free software; you can redistribute it and/or modify it | ||
| * under the terms of the GNU General Public License version 2 only, as | ||
| * published by the Free Software Foundation. Codename One designates this | ||
| * particular file as subject to the "Classpath" exception as provided | ||
| * by Oracle in the LICENSE file that accompanied this code. | ||
| * | ||
| * This code is distributed in the hope that it will be useful, but WITHOUT | ||
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | ||
| * version 2 for more details (a copy is included in the LICENSE file that | ||
| * accompanied this code). | ||
| * | ||
| * You should have received a copy of the GNU General Public License version | ||
| * 2 along with this work; if not, write to the Free Software Foundation, | ||
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | ||
| * | ||
| * Please contact Codename One through http://www.codenameone.com/ if you | ||
| * need additional information or have any questions. | ||
| */ | ||
| package com.codename1.analytics.invite; | ||
|
|
||
| /// Reads the invite code an iOS App Clip left behind for the full | ||
| /// application. | ||
| /// | ||
| /// This is the iOS half of deterministic attribution, and the counterpart of | ||
| /// [InstallReferrerSource] on Android. An App Clip is launched by the invite | ||
| /// link itself and receives that link exactly, so it can write the code into | ||
| /// the container it shares with the full application before offering the App | ||
| /// Store. When the person installs, the application reads it here: the code | ||
| /// made the whole trip through the store, so nothing is matched or guessed. | ||
| /// | ||
| /// It replaced a statistical match against a hashed device profile, which | ||
| /// existed only because the App Store carries no referrer of its own. Nothing | ||
| /// about the visitor is collected any more. | ||
| /// | ||
| /// The Codename One build supplies the implementation on platforms that have | ||
| /// one and registers it through [Invites#registerAppClipHandoffSource] before | ||
| /// the application starts. Where none is registered -- the simulator, the | ||
| /// desktop build, Android, and any iOS application built without an App Clip | ||
| /// -- [Invites] behaves exactly as it does when a clip left nothing. | ||
| /// | ||
| /// An application does not implement this interface. | ||
| public interface AppClipHandoffSource { | ||
| /// Whether this source can answer at all on the current device. | ||
| /// | ||
| /// #### Returns | ||
| /// | ||
| /// true when a shared container is reachable | ||
| boolean isSupported(); | ||
|
|
||
| /// Asks for the code an App Clip left behind. The answer arrives on the | ||
| /// callback, possibly asynchronously and possibly on another thread; | ||
| /// [Invites] marshals it back onto the EDT. | ||
| /// | ||
| /// The handoff is read once and cleared by the implementation, so a code | ||
| /// cannot be claimed twice by two launches. | ||
| /// | ||
| /// #### Parameters | ||
| /// | ||
| /// - `callback`: receives the answer, never null | ||
| void requestHandoff(AppClipHandoffCallback callback); | ||
| } |
52 changes: 52 additions & 0 deletions
52
CodenameOne/src/com/codename1/analytics/invite/InstallReferrerCallback.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| /* | ||
| * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. | ||
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | ||
| * This code is free software; you can redistribute it and/or modify it | ||
| * under the terms of the GNU General Public License version 2 only, as | ||
| * published by the Free Software Foundation. Codename One designates this | ||
| * particular file as subject to the "Classpath" exception as provided | ||
| * by Oracle in the LICENSE file that accompanied this code. | ||
| * | ||
| * This code is distributed in the hope that it will be useful, but WITHOUT | ||
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | ||
| * version 2 for more details (a copy is included in the LICENSE file that | ||
| * accompanied this code). | ||
| * | ||
| * You should have received a copy of the GNU General Public License version | ||
| * 2 along with this work; if not, write to the Free Software Foundation, | ||
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | ||
| * | ||
| * Please contact Codename One through http://www.codenameone.com/ if you | ||
| * need additional information or have any questions. | ||
| */ | ||
| package com.codename1.analytics.invite; | ||
|
|
||
| /// Receives the answer from an [InstallReferrerSource]. | ||
| /// | ||
| /// Implemented by the framework; an application never implements this. | ||
| public interface InstallReferrerCallback { | ||
| /// Called with the raw referrer query string the store recorded at | ||
| /// install time. | ||
| /// | ||
| /// #### Parameters | ||
| /// | ||
| /// - `rawReferrer`: the undecoded referrer query string, may be empty | ||
| /// | ||
| /// - `referrerClickSeconds`: when the link was clicked, in seconds since | ||
| /// the epoch, or 0 when the store did not say | ||
| /// | ||
| /// - `installBeginSeconds`: when the install began, in seconds since the | ||
| /// epoch, or 0 when the store did not say | ||
| void onReferrer(String rawReferrer, long referrerClickSeconds, | ||
| long installBeginSeconds); | ||
|
|
||
| /// Called when no referrer can be obtained. This is the normal answer on | ||
| /// a device with no store client -- a sideload, an emulator without store | ||
| /// services, or a non-store distribution -- and is not an error. | ||
| /// | ||
| /// #### Parameters | ||
| /// | ||
| /// - `reason`: one of the `REASON_` constants on [Invites] | ||
| void onUnavailable(String reason); | ||
| } |
54 changes: 54 additions & 0 deletions
54
CodenameOne/src/com/codename1/analytics/invite/InstallReferrerSource.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| /* | ||
| * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. | ||
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | ||
| * This code is free software; you can redistribute it and/or modify it | ||
| * under the terms of the GNU General Public License version 2 only, as | ||
| * published by the Free Software Foundation. Codename One designates this | ||
| * particular file as subject to the "Classpath" exception as provided | ||
| * by Oracle in the LICENSE file that accompanied this code. | ||
| * | ||
| * This code is distributed in the hope that it will be useful, but WITHOUT | ||
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | ||
| * version 2 for more details (a copy is included in the LICENSE file that | ||
| * accompanied this code). | ||
| * | ||
| * You should have received a copy of the GNU General Public License version | ||
| * 2 along with this work; if not, write to the Free Software Foundation, | ||
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | ||
| * | ||
| * Please contact Codename One through http://www.codenameone.com/ if you | ||
| * need additional information or have any questions. | ||
| */ | ||
| package com.codename1.analytics.invite; | ||
|
|
||
| /// Reads the referrer the application store recorded when this application | ||
| /// was installed. This is the deterministic half of invite attribution: the | ||
| /// invite code makes the whole round trip through the store, so no matching | ||
| /// or guessing is involved. | ||
| /// | ||
| /// The Codename One build supplies the implementation on platforms that have | ||
| /// one and registers it through | ||
| /// [Invites#registerInstallReferrerSource] before the application starts. | ||
| /// Where none is registered -- the simulator, the desktop build, iOS, and any | ||
| /// Android device without store services -- [Invites] behaves exactly as it | ||
| /// does on a device that reports no referrer. | ||
| /// | ||
| /// An application does not implement this interface. | ||
| public interface InstallReferrerSource { | ||
| /// Whether this source can answer at all on the current device. | ||
| /// | ||
| /// #### Returns | ||
| /// | ||
| /// true when a store client is present | ||
| boolean isSupported(); | ||
|
|
||
| /// Asks for the install referrer. The answer arrives on the callback, | ||
| /// possibly asynchronously and possibly on another thread; [Invites] | ||
| /// marshals it back onto the EDT. | ||
| /// | ||
| /// #### Parameters | ||
| /// | ||
| /// - `callback`: receives the answer, never null | ||
| void requestReferrer(InstallReferrerCallback callback); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.