Skip to content

Conversation

sputh
Copy link
Contributor

@sputh sputh commented Dec 4, 2019

This is especially useful when you don't want to track any analytics or drop any analytic cookies prior to asking the users to opt in to analytics.
I've added a configuration (deferInitialization) and an API (enableTracking) to manage this.
If deferInitialization is set to true AND a customer has yet to opt in, we will locally store their pending logs until they either navigate away or opt in.

Note: This is a little gross because I'm checking whether or not to defer calling the function in EVERY public function. I considered using js decorators but it would require refactoring this to use classes. I considered iterating over an array of public methods and wrapping them, but that makes debugging much more difficult in the future. Ultimately, I opted for readability.

This is especially useful when you don't want to track any analytics or drop any analytic cookies prior to asking the users to opt in to analytics.
I've added a configuration (deferInitialization) and an API (enableTracking) to manage this.
If deferInitialization is set to true AND a customer has yet to opt in, we will locally store their pending logs until they either navigate away or opt in.
});
});

describe('upon to opting into analytics', function () {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: fix english 🤦🏻‍♀️

@haoliu-amp
Copy link
Contributor

Look good to me, but would leave approval decision to Dan or Krishna.

@djih djih self-requested a review December 5, 2019 20:52
Copy link
Member

@djih djih left a comment

Choose a reason for hiding this comment

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

I think we need to discuss the API and how the customer might interact with this. It looks like you're requiring the customer to figure out if the user has opted in or out every single time on page load, and call enable tracking?

Does it make sense for us to track whether a user has opted in? Once the user opts in, we can skip all the defer initialization logic too. I feel like we should discuss this API and the customer developer interaction

A side note: I feel like adding if (this._shouldDeferCall()) { return this._q.push(['setDomain', domain]); }
is fragile. If one of us has a typo in the function name, or forgets one of the args, it will break. I wonder if there's a way to programatically do it, like how the snippet does arguments.slice to grab all the arguments: https://github.com/amplitude/Amplitude-JavaScript/blob/master/src/amplitude-snippet.js#L33

*/
AmplitudeClient.prototype._deferInitialization = function _deferInitialization(apiKey, opt_userId, opt_config, opt_callback) {
this._initializationDeferred = true;
this._q.push(['init', apiKey, opt_userId, opt_config, opt_callback]);
Copy link
Member

Choose a reason for hiding this comment

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

is this _q already used for something else? Are there any potential risks for re-using it and having the two different use cases collide? I feel like it's safer to use a separate queue for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it doesn't seem like there's any potential risk to re-using it. I think it would add more confusion if we had multiple queues that are vaguely defined

Copy link
Member

Choose a reason for hiding this comment

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

👍

Copy link
Member

Choose a reason for hiding this comment

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

I hope this doesn't cause some weird circular queueing behavior where you defer a function that pushes to this or something, etc 🤔 (seems fine I guess?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i can't imagine it having circular dependency issues 🤔
since it's a queue, it will run functions in order and it can tolerate if we pass in a function multiple times

}

var hasExistingCookie = !!this.cookieStorage.get(this.options.cookieName + this._storageSuffix);
if (opt_config && opt_config.deferInitialization && !hasExistingCookie) {
Copy link
Member

Choose a reason for hiding this comment

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

ok so let's say a user accepts and allows tracking and revisits the page the next day. There would already be an existing cookie and we would skip _deferInitialization. However, wouldn't this._shouldDeferCall() still return true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No this._shouldDeferCall() would return false (for the most part, there's a short period of time in the init function where this._pendingReadStorage returns true while getting started).

It depends on how they revisit the page. If the page never refreshed and they have the same instance of Amplitude, then the new events will not be sent --- I've added a test for this

If it's a new instance of amplitude (e.g refresh page, navigate to another page), then hasExistingCookie will be true and _initializationDeferred will never be set to true, thus this._shouldDeferCall() will be false as long as this._pendingReadStorage is also false (most of the time)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After adding tests, and further inspecting, i realize you're right. Because _initializationDeferred is never set to false AND this._storageSuffix isn't defined until the try block below, it's always assuming _initializationDeferred is true.

I've added a test to reflect it and additional assert statements for the existing tests

Copy link
Member

@djih djih left a comment

Choose a reason for hiding this comment

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

Also don't forget to add your new enable tracking method to the snippet

…g (when a user explicitly opts into analytics), move deferInitialization check where this._storageSuffix is actually defined (in the try block)

* set initializedDeferred = false in enableTracking()
* make sure this._storageSuffix is defined when cookies are being checked
* Added tests for sending events after tracking is enabled
* Added assertions to ensure that each test starts with a clean slate (no unsentEvents) before enabling tracking
this.options.apiKey = apiKey;
this._storageSuffix = '_' + apiKey + this._legacyStorageSuffix;

var hasExistingCookie = !!this.cookieStorage.get(this.options.cookieName + this._storageSuffix);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved into the try catch to ensure that this._storageSuffix is defined

Copy link
Member

Choose a reason for hiding this comment

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

i kind of like that you're using the cookie to signal if the user has accepted tracking

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i feel like that's the safest bet to carry over. i haven't fully thought of the case where our cookie expires tho.... 😬

Copy link
Member

@djih djih left a comment

Choose a reason for hiding this comment

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

If it's not too much of a hassle can you add a test case for verifying that deferring is skipped if the user already has a cookie

this.options.apiKey = apiKey;
this._storageSuffix = '_' + apiKey + this._legacyStorageSuffix;

var hasExistingCookie = !!this.cookieStorage.get(this.options.cookieName + this._storageSuffix);
Copy link
Member

Choose a reason for hiding this comment

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

i kind of like that you're using the cookie to signal if the user has accepted tracking

this._storageSuffix = '_' + apiKey + this._legacyStorageSuffix;

var hasExistingCookie = !!this.cookieStorage.get(this.options.cookieName + this._storageSuffix);
if (opt_config && opt_config.deferInitialization && !hasExistingCookie) {
Copy link
Member

Choose a reason for hiding this comment

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

note for existing users, they won't get the option to defer tracking because they will already have a cookie, i guess we just need to communicate that to our customers and the people that requested this feature

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, this will only work on new users without existing cookies and they have to add the config flag

*/
AmplitudeClient.prototype.setDomain = function setDomain(domain) {
if (this._shouldDeferCall()) {
return this._q.push(['setDomain', domain]);
Copy link
Member

Choose a reason for hiding this comment

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

is it weird to return the result of array.push? or are you trying to shorthand

{
   this._q.push...
   return
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

shorthand. im doing this for the set__ where it's not expecting anything in return

*/
AmplitudeClient.prototype._deferInitialization = function _deferInitialization(apiKey, opt_userId, opt_config, opt_callback) {
this._initializationDeferred = true;
this._q.push(['init', apiKey, opt_userId, opt_config, opt_callback]);
Copy link
Member

Choose a reason for hiding this comment

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

I hope this doesn't cause some weird circular queueing behavior where you defer a function that pushes to this or something, etc 🤔 (seems fine I guess?)

sputh added 2 commits December 6, 2019 13:33
…ice.call(arguments) and add additional tests to verify that existing users (e.g. users with an amplitude cookie) will continue to be logged
@sputh sputh force-pushed the feature/defer-init branch from c1af073 to 44207f3 Compare December 6, 2019 21:44
@sputh
Copy link
Contributor Author

sputh commented Dec 6, 2019

@djih for sanity's sake, I took your suggestion and added a test to ensure that existing users (e.g. user already has a cookie), will continue to send events even if deferInitialization is set to true

I've also updated the snippet and the minor version.

@sputh sputh merged commit 6bc9065 into amplitude:master Dec 6, 2019
@sputh sputh deleted the feature/defer-init branch December 6, 2019 21:55
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.

4 participants