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

Add experiment state #38907

Merged
merged 16 commits into from
Jan 29, 2020
Merged

Add experiment state #38907

merged 16 commits into from
Jan 29, 2020

Conversation

withinboredom
Copy link
Contributor

Changes proposed in this Pull Request

It's not clear if CURRENT_USER_RECIEVE is required. I'm going to leave it there for now.

This code doesn't do anything currently except adding some reducers. The getAnonId part of the reducer is the riskiest piece of this and close monitoring will be required after deployment.

Testing instructions

  • npm test to run the unit tests

@withinboredom withinboredom added [Pri] Normal [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. [Tests] Includes Tests [Type] New feature ABTest labels Jan 17, 2020
@withinboredom withinboredom requested review from blowery and a team January 17, 2020 15:22
@withinboredom withinboredom self-assigned this Jan 17, 2020
@matticbot
Copy link
Contributor

@withinboredom withinboredom changed the title Add/experiment state Add experiment state Jan 17, 2020
Copy link
Contributor

@yanirs yanirs left a comment

Choose a reason for hiding this comment

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

Left some comments based on my limited understanding of the code. I'm not familiar enough with Calypso to provide very useful feedback on how everything connects, so it'd be good to get @blowery's review.

client/state/experiments/reducer.ts Outdated Show resolved Hide resolved
client/state/experiments/reducer.ts Outdated Show resolved Hide resolved
client/state/experiments/test/reducer.js Outdated Show resolved Hide resolved
*
* @param state The application state
*/
export const getAnonId = ( state: AppState ) => get( state, [ 'experiments', 'anonId' ], null );
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to expose the anonId from the state? It seems like an internal detail to me, but I don't have a good mental image of how everything connects.

Also, Get's -> Gets 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we need to expose the anonId from the state?

It appears the cookie gets deleted at some point... so we can't rely on it sticking around. We need it in case someone hits a landing page with an anon-experiment and gets assigned to that experiment. When they login, we need to know what they were assigned to as an anon-user so we need the value of the cookie to send to the assignment API to let it know that this user was that anon-user.

Copy link
Contributor

Choose a reason for hiding this comment

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

Make sense. I was thinking more about whether we should export this function as opposed to using the anon ID internally and still storing it in the state.

Another thing I thought about is the order of execution: Is it possible for our code to run before the code that sets the cookie? Looks like it sometimes happens on the client side:

if ( ! _ui ) {
const cookies = cookie.parse( document.cookie );
if ( cookies.tk_ai ) {
_ui = cookies.tk_ai;
} else {
const randomIdLength = 18; // 18 * 4/3 = 24 (base64 encoded chars).
_ui = createRandomId( randomIdLength );
document.cookie = cookie.serialize( 'tk_ai', _ui );
}
}

However, from poking around it looks like there's also server-side code to set the same cookie. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe we should make a new cookie instead of fighting over the tracks anon cookie?

standards

(I don't think that's a bad idea btw.) However, we should probably recheck the anon cookie before making the API request.

Copy link
Contributor

Choose a reason for hiding this comment

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

We need to use a consistent anonId to match our event tracking. It looks like initialising the analytics module would ensure that the cookie exists, but is there a better way to ensure it happens?

Taking a step back, is the only purpose of saving the anonId to pass it to an API call? If so, can't we assume that the server knows the anonId? If we can't make this assumption, how would the server track assignment for anons? Rely on the passed anonId?

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've verified that it doesn't capture the anon-id from a fresh boot of calypso and an anon user. I'll think on this and get back to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, after a few more tests, it does capture the anonid WHEN the user logs in, but not before.

Thinking about it a bit more, this is what I'm going to do:

  1. Continue to capture the anonid on app init. This will capture it if they come from a landing page or something.
  2. Capture the anonid if we haven't already, when we request from the API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will provide any guarantee that the cookie will exist.

There's no guarantee the cookie will exist (it gets deleted when the user is aliased as far as I can tell). All we can guarantee is "best effort".

Copy link
Contributor

@bperson bperson Jan 23, 2020

Choose a reason for hiding this comment

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

( thinking more about this )

It appears the cookie gets deleted at some point...

If it can be useful: this most likely happens in w.js when the user gets identified with identifyUser ( but I'm not sure exactly when that happens anymore in Calypso' flows )

edit 1: oh well should have refreshed before commenting :D

edit 2: fwiw, if we think about NOT deleting that cookie on identifyUser, I don't think it's a good idea

Copy link
Member

Choose a reason for hiding this comment

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

Actually, I don't think the analytics module will provide any guarantee that the cookie will exist.

Why not? analytics.initialize() calls checkForBlockedTracks(), which calls the code I referenced, though it does happen in the catch() of _loadTracksResult so I'm not sure about the order of execution. @yanirs

checkForBlockedTracks will only initialize the anonymous ID if the loadScript( '//stats.wp.com/w.js?60' ) fails.

Even if the network request does fail, it'd be too late; the analytics module is initialized as a middleware after the Redux store has already been initialized.

FWIW, landing pages use a different boot logic.

client/state/experiments/actions.ts Show resolved Hide resolved
@withinboredom
Copy link
Contributor Author

withinboredom commented Jan 20, 2020

This is the original bundle size.

image

This is the new bundle size reusing the existing anonId cookie function.

image

It increases the initial bundle size by 99kb but decreases all other bundles by a total of 500kb. This is an acceptable tradeoff for me since it makes our app overall less expensive to download, at a small expense up-front.

It adds about +3s upfront on a bad (1 bar) 3g connection (but no impact on anything faster) but saves 16s on other components. Since 100kb has no impact on other speeds, it will save time on anything faster than a bad 3g speed since the other bundles get smaller.

Copy link
Member

@jsnmoon jsnmoon left a comment

Choose a reason for hiding this comment

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

Code looks reasonable 👍

We'll want to figure out a way to guarantee the existence of the _ui cookie value, though; left a comment inline regarding this issue.

@@ -1466,7 +1466,7 @@ function floodlightUserParams() {
*
* @returns {string} - The Tracks anonymous user id
*/
function tracksAnonymousUserId() {
Copy link
Member

Choose a reason for hiding this comment

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

Outside the scope of this PR, but this function is a duplicate of this function here.

*
* @param state The application state
*/
export const getAnonId = ( state: AppState ) => get( state, [ 'experiments', 'anonId' ], null );
Copy link
Member

Choose a reason for hiding this comment

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

It looks like initialising the analytics module would ensure that the cookie exists, but is there a better way to ensure it happens?

Actually, I don't think the analytics module will provide any guarantee that the cookie will exist. We typically depend on Tracks' w.js to serializes the _ui cookie value.

I think following @yanirs' advice here makes the most sense.

Pinging @bperson and @xyu for guidance.

@withinboredom
Copy link
Contributor Author

rebased

*/
export const fetchExperiments = ( anonId: string ) => ( {
type: EXPERIMENT_FETCH,
anonId: anonId == null ? getAnonIdFromCookie() : anonId,
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably worth adding an inline comment explaining why we do 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.

That's probably a good idea.

Copy link
Contributor

@yanirs yanirs 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 can move forward with merging and push any remaining work on the anonId to an issue.

@gwwar gwwar requested a review from a team January 23, 2020 22:24
@dmsnell
Copy link
Contributor

dmsnell commented Jan 23, 2020

Concerning the app bundle size I'd not look as much at the reduction in section bundles but whether most page views will rely on this. It's not worth 99kb in the initial bundle if people only need the code once every 1,000 page views.

If there is concern about the additional bundle size then we might be missing an opportunity to have webpack split this out on its own. That is, we might be missing some hints to webpack that these chunks should all come together in one bundle instead of being distributed in pieces and duplicated in twenty other bundles.

client/state/experiments/reducer.ts Outdated Show resolved Hide resolved
client/state/experiments/reducer.ts Outdated Show resolved Hide resolved
client/state/experiments/reducer.ts Outdated Show resolved Hide resolved
client/state/experiments/reducer.ts Outdated Show resolved Hide resolved
client/state/experiments/selectors.ts Outdated Show resolved Hide resolved
@jsnajdr
Copy link
Member

jsnajdr commented Jan 24, 2020

It increases the initial bundle size by 99kb but decreases all other bundles by a total of 500kb. This is an acceptable tradeoff for me since it makes our app overall less expensive to download, at a small expense up-front.

The reported ICFY stats are wrong for some reason. The latest icfy-stats CI task failed by running out of memory, and the stats reported for the previous push are different from what I see locally.

This patch only adds one small reducers to state/, and doesn't import any new modules that were not already there.

I recommend rebasing onto the latest master and letting the icfy-stats task run again. The updated stats should be correct.

Increasing the initial bundle by 100kB would be a bad tradeoff, adding more than 10% to the entrypoint. Many routes would need to load extra code even when they never call it.

The app would get less expensive to download only if the user's browser wanted to download all sections' chunks. But the user usually navigates only to one, two or three sections, never through the whole Calypso. The initial load would get slower.

Copy link
Member

@dzver dzver left a comment

Choose a reason for hiding this comment

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

LGTM

@matticbot
Copy link
Contributor

matticbot commented Jan 24, 2020

Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

App Entrypoints (~178 bytes added 📈 [gzipped])

name                   parsed_size           gzip_size
entry-main                  +628 B  (+0.0%)     +152 B  (+0.0%)
entry-login                  +55 B  (+0.0%)       +8 B  (+0.0%)
entry-gutenboarding          +55 B  (+0.0%)       +8 B  (+0.0%)
entry-domains-landing        +55 B  (+0.0%)      +10 B  (+0.0%)

Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used.

Legend

What is parsed and gzip size?

Parsed Size: Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
Gzip Size: Compressed size of the JS and CSS files. This much data needs to be downloaded over network.

Generated by performance advisor bot at iscalypsofastyet.com.

@withinboredom
Copy link
Contributor Author

withinboredom commented Jan 24, 2020

If those stats are correct, then this is the time to download:

section size difference (kb) time saved (seconds @ slow 3g) time saved (seconds @ regular 3g)
entry-jetpack-cloud -24.76 +0.79 +0.03
entry-main +12.53 -0.40 -0.01
entry-login +12.39 -0.40 -0.01
entry-gutenboarding +12.37 -0.40 -0.01
entry-domains-landing +12.37 -0.40 -0.01
posts-custom -185.96 +5.95 +0.21
posts -26.72 +0.86 +0.03
post-editor -19.94 +0.64 +0.02
activity -19.94 +0.64 +0.02
stats -16.20 +0.52 +0.02
comments -6.89 +0.22 +0.01
woocommerce +6.73 -0.22 -0.01
purchases +6.73 -0.22 -0.01
checkout +6.73 -0.22 -0.01
marketing -3.00 +0.10 +0.00

It would be nice if it did this for you :) let me know where I can make a PR to add these time calculations if you think they'd be useful. I think these are all pretty reasonable (dunno what's going on with posts-custom though...)

@withinboredom withinboredom force-pushed the add/experiment-state branch 2 times, most recently from 6143db7 to ca39a23 Compare January 29, 2020 12:05
@withinboredom withinboredom added [Status] Ready to Merge and removed [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. labels Jan 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants