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

User Sync Module #1229

Closed
wants to merge 44 commits into from
Closed

Conversation

grevory
Copy link
Contributor

@grevory grevory commented May 24, 2017

Type of change

  • Feature

Description of change

Added a user sync module for aggregating and handling all syncs for all adapters. This first phase of user sync only uses image pixels. The next phase will include iframes and ajax which will be configurable by the publisher.

Other information

Expands on the cookie sync changes recently added for prebid server

@grevory grevory changed the title Improvement/user sync User Sync Module May 24, 2017
@snapwich
Copy link
Collaborator

this is related to #1088

src/userSync.js Outdated
* @private
*/
function fireSyncs() {
let bodyElem = document.getElementsByTagName('body')[0];
Copy link
Member

Choose a reason for hiding this comment

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

We should make sure this isn't undefined, and fall back to head as the insertion point (common in prebid.js setups).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool. Will implement.

src/userSync.js Outdated
try {
// Fire image pixels
queue.image.forEach((sync) => {
let bidderName = sync[0];
Copy link
Member

Choose a reason for hiding this comment

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

these can all be const

src/userSync.js Outdated
if (!url) {
return;
}
const img = hideAndIdElem(new Image());
Copy link
Member

Choose a reason for hiding this comment

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

this function seems a bit... odd. Is there reason to have all these smaller functions so you can compose other things in the future?

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 was trying to keep everything small and functional. But yea, I do not expect this will be reused.

src/userSync.js Outdated
return;
}
const img = hideAndIdElem(new Image());
img.src = encodeURI(url);
Copy link
Member

Choose a reason for hiding this comment

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

Should we make encoding the URL an option (default 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.

I was thinking we would just watch to see if adapters have their URLs encoded already in their PRs rather than checking programmatically.

src/userSync.js Outdated
}
const img = hideAndIdElem(new Image());
img.src = encodeURI(url);
if (removeOnLoad) {
Copy link
Member

Choose a reason for hiding this comment

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

why would you not remove it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There were tests that ran without passing that value but I removed them.

src/userSync.js Outdated
thisImg.parentNode.removeChild(thisImg);
}
catch (e) {
utils.logWarn('Could not remove image pixel element', e);
Copy link
Member

Choose a reason for hiding this comment

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

don't think we need to warn about this... not actionable.

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. Thought it would be useful for debugging.

src/userSync.js Outdated
* // registerSync(type, adapter, pixelUrl)
* userSync.registerSync('image', 'rubicon', 'http://example.com/pixel')
*/
userSync.registerSync = function(type, bidder, ...data) {
Copy link
Member

Choose a reason for hiding this comment

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

Should we make the 3rd argument just string[] for simplicity?

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 made it spread for a future implementation, specially ajax. It is in the spec.

Copy link
Contributor

@slimkrazy slimkrazy Jun 8, 2017

Choose a reason for hiding this comment

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

Will it not be cleaner to make this API accept an object as the sole parameter? It's easily extended, but is easier to read and easier to implement an API call for. Additionally, lines of code like line 18 and 19 will benefit in terms of readability, too.

Also - how about surfacing some constants that folks can use for the supported types?

userSync.registerSync({
type: userSync.TYPE_IMAGE,
bidder: 'foo',
url: 'http://pixerserver.com/bar/baz'
});

Copy link
Contributor

@dbemiller dbemiller Jun 8, 2017

Choose a reason for hiding this comment

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

Strong +1 with @slimkrazy here. Attaching semantics to list elements is both error-prone (for callers) and unclear (for readers).

If this API were extended with 5-6 new values, it'd only be a matter of time before someone felt like they could improve the code with const BIDDER_NAME_INDEX = 1; constants, because those list accesses were in fact magic numbers.

Better to just cut that cycle short and start out with the map and named keys.

mkendall07
mkendall07 previously approved these changes May 26, 2017
Copy link
Member

@mkendall07 mkendall07 left a comment

Choose a reason for hiding this comment

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

looks good! Final question, do we want to limit the syncUsers function to once per page load? (don't fire for refresh bids?)

@grevory
Copy link
Contributor Author

grevory commented May 26, 2017

I would presume they wouldn't need the user sync once they have it, but it is also not blocking the auction. It could block the adserver call I guess. @bretg What do you think?

@bretg
Copy link
Collaborator

bretg commented May 30, 2017

do we want to limit the syncUsers function to once per page load?

agreed

@grevory
Copy link
Contributor Author

grevory commented May 31, 2017

So each sync will only fire once, but what about syncs that are added late for whatever reason. They should be available for future auctions.

@bretg
Copy link
Collaborator

bretg commented May 31, 2017

Proposing that userSync on refresh is a possible future feature, but not 1.0. Syncs may be registered after the initial auction and still be part of the sync-drop, but after that drop, it's too late.

src/userSync.js Outdated
*/
userSync.syncUsers = function(timeout = 0) {
setTimeout(fireSyncs, timeout);
};
Copy link
Contributor

@dbemiller dbemiller Jun 1, 2017

Choose a reason for hiding this comment

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

Checking my understanding here: isn't the goal here is to prevent the user sync from interfering with the auction? If so, making the auction methods async is a much more reliable way to do it.

Imagine yourself as a publisher here. How would you choose an appropriate timeout? What if half your users are in countries with extremely slow machines? The problem is fundamentally impossible to solve reliably... and some publishers might not even have much of a tech department to discuss the tradeoffs with.

This is what callbacks (or Promises) were made for. Both reliably execute some code after a long-running task is done... and since they wouldn't touch the external API, none of our users have to worry about how to configure it properly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not really. Most publishers have an average page load time before which they will not want the user sync to execute. So if a publisher knows their average page load is 5s, they may pass in a timeout of 5100 to delay the execution so the user sync doesn't slow down the page load.

We have already identified a default of 0 for the timeout delay as too low and will be updating before this merges.

Thanks.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you hit the nail on the head. If their average page load time is 5s, then a timeout of 5100s is a reasonable choice.

The timeout is just an indirect means of accomplishing the real goal. In your example, it's "after page loads"

I'm just having a hard time imagining when it's the appropriate choice.

For simple pages, they should userSync on the load event.
For pages running scripts with a setTimeout call, they should userSync at the end of the callback.
For complex pages which lots of js which needs to be executed in small bits to avoid page jank, then they should be adding the userSync calls to whatever async-queue they're using.

There may be pages out there where a fixed timeout is actually the best implementation choice... but I feel like I must raise the question: is it really a wise decision to hardcode that choice into prebid?

Copy link
Collaborator

Choose a reason for hiding this comment

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

what about using:

document.addEventListener("DOMContentLoaded", function(event) { 
  //do work
});

Copy link
Contributor

Choose a reason for hiding this comment

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

prebid is loaded async, so DOMContentLoaded wouldn't be reliable. If anything you'd want load...

...but as I mentioned, that might still interfere with the page load experience if the pub has javascript inside an unfortunately-timed setTimeout that does a bunch of processing at the same time the load event fires.

It seems like any default behavior chosen here will negatively impact some sites. Letting the pubs initiate (and prevent the default behavior) is at least safe because they can decide when best to run the sync on their site.

Copy link
Member

Choose a reason for hiding this comment

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

How about just defaulting to load event?

@bretg
Copy link
Collaborator

bretg commented Jun 5, 2017

thanks for the good feedback @dbemiller - we'll support an option for pubs to initiate the sync.

@grevory grevory added the on hold label Jun 5, 2017
src/userSync.js Outdated
if (!url) {
return;
}
let img = hideAndIdElem(new Image());
Copy link
Contributor

Choose a reason for hiding this comment

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

Using the hideAndIdElem function for an in-memory image is really not required, right? I'm guessing that hideAndIdElem was created for iframe support? Which leads me nicely onto my next question: Can iframe support go into this initial implementation? It would make sense if the goal is for bidders to adopt using this util over creating their own routines dealing with user syncing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are correct. Iframe and ajax support will be nipping on the heals of our initial release of user sync. Right now this is on hold while we implement some enhancements we discussed internally.

src/userSync.js Outdated
// insertAdjacentHTML expects HTML string - convert DOM object to string
let img = userSync.createImgObject(trackingPixelUrl);
if (img) {
utils.insertElement(img);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you need to insert the image element into the DOM? Can't in-memory with the src set, do?

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 have not used srcset for triggering tracking pixels. How does it work? Each URL in the list gets fired? I will need to look into it.

@protonate protonate self-assigned this Jun 20, 2017
const img = new Image();
img.id = _getUniqueIdentifierStr();
img.src = url;
Copy link
Member

Choose a reason for hiding this comment

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

Why was this code dropped? I realize that chrome requests this URL anyway even if the image is not inserted into the DOM but is this universal? Is a spec on it?

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 think it was @dbemiller who made the suggestion to not add elements to the DOM which we don't need to. I believe my research suggested that simply creating the JS image object with a src was enough for all browsers.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not I... but I do agree with whoever said it, as long as you're sure it works cross-browser :).

If you don't into the DOM, it shouldn't cause a re-paint of the page, which can have huge performance impact on pub pages.

Copy link
Collaborator

Choose a reason for hiding this comment

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

my experience is that just creating the image with a src works on all major browsers, but don't have an authoritative source.

Copy link
Member

Choose a reason for hiding this comment

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

I think it's fine being OK for this use case, I'm thinking about when this is used to count an impression we need to make sure it works. I'm ok as long as this was tested in all the browsers we care about.

src/userSync.js Outdated
* @param {boolean} enableOverride Tells this module to expose the syncAll method to the public
* @public
*/
publicApi.overrideSync = (enableOverride) => {
Copy link
Member

Choose a reason for hiding this comment

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

Ideally the only file that mutates the pbjs global would be prebid.js. I discussed this with @dbemiller and he suggested just putting this code into src/prebid.js - example:

import { userSync } from 'src/userSync';

if ($$PREBID_GLOBAL$$.userSync.enableOverride) {
  $$PREBID_GLOBAL$$.userSync.syncAll = userSync.syncUsers;
}

src/userSync.js Outdated
@@ -0,0 +1,175 @@
import * as utils from 'src/utils';

export function newUserSync() {
Copy link
Member

@mkendall07 mkendall07 Aug 15, 2017

Choose a reason for hiding this comment

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

edit: nevermind

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This follows the pattern in config (https://github.com/prebid/Prebid.js/blob/master/src/config.js#L28). It allows for resetting the state of this module in between tests.

let queue = getDefaultQueue();

// Since user syncs require cookie access we want to prevent sending syncs if cookies are not supported
let cookiesAreSupported = !utils.isSafariBrowser() && utils.cookiesAreEnabled();
Copy link
Member

Choose a reason for hiding this comment

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

do we want to block this or leave it to the caller? Talking about safari specifically.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was a decision made by the group. @bretg what is your opinion here?

src/userSync.js Outdated

// Merge the defaults with the user-defined config
let userSyncConfig = Object.assign($$PREBID_GLOBAL$$.userSync || {},
userSyncDefaultConfig);
Copy link
Member

Choose a reason for hiding this comment

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

Should we pass in the userSync object or have it set in the constructor? This is a basically undocumented dependency.

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 fair. I thinking passing it in is fine. I think it would make testing easier too.

src/userSync.js Outdated
* userSync.registerSync('image', 'rubicon', 'http://example.com/pixel')
*/
publicApi.registerSync = (type, bidder, ...data) => {
if (!userSyncConfig.syncEnabled || !utils.isArray(queue[type])) {
Copy link
Member

Choose a reason for hiding this comment

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

this should protect again undefined bidder being sent.


it('should register and fire a pixel URL', () => {
userSync.registerSync('image', 'testBidder', 'http://example.com');
userSync.syncUsers();
Copy link
Collaborator

Choose a reason for hiding this comment

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

are there any tests that verify that syncUsers() is called when an auction finishes, rather than calling it directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds like an integration test.

src/userSync.js Outdated
* // registerSync(type, adapter, pixelUrl)
* userSync.registerSync('image', 'rubicon', 'http://example.com/pixel')
*/
publicApi.registerSync = (type, bidder, ...data) => {
Copy link
Contributor

@dbemiller dbemiller Aug 21, 2017

Choose a reason for hiding this comment

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

Would strongly prefer a @param {SyncData} data, with a @typedef {object} SyncData (and associated @param tags) here.

As someone who wants to call this method, this is not self-documenting. Rules like "the first arg of this ...data is the URL" are invisible, and the only way to figure them out is to read the code.

Since this method is adapter-facing, I think it's very important to make this as clear as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since we are no longer support AJAX we can make this fix without causing a mess. I will do it now. AJAX support made it complicated.

@grevory grevory dismissed mkendall07’s stale review August 29, 2017 20:45

We are accepting the proposed change as is.

* pixelEnabled: true,
* syncsPerBidder: 5,
* syncDelay: 3000,
* iframeEnabled: true
Copy link
Member

Choose a reason for hiding this comment

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

So by turning on the Rubicon adapter, you automatically enable iframe user sync?

* @summary A `syncUsers` wrapper for determining if enableOverride has been turned on
* @public
*/
publicApi.syncUsersOverride = () => {
Copy link
Member

Choose a reason for hiding this comment

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

prefer this name to match what's in prebid.js so triggerUserSyncs for consistency.

queue.length = 0;
}

function setBidderSynced(bidder) {
Copy link
Member

Choose a reason for hiding this comment

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

I'd like to preserve this functionality if we can. How about a callback on the pixel queue?

Copy link
Member

@mkendall07 mkendall07 left a comment

Choose a reason for hiding this comment

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

Need to move changes out of cookie.js into prebid adapter or into cookieSync

@mkendall07 mkendall07 closed this Aug 31, 2017
@robertrmartinez robertrmartinez deleted the improvement/user-sync branch July 5, 2023 19:45
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.

9 participants