User Activation v2 lets sites accidentally trigger an install prompt on an unrelated click event |
|||
Issue descriptionChrome Version: 72 OS: Chrome OS, Linux, Windows What steps will reproduce the problem? (1) Open https://killer-marmot.appspot.com/web/?action=preempt (2) Note the red message "prompt() rejected: NotAllowedError: The prompt() method must be called with a user gesture" (3) Now, refresh the page, and as the page is loading, click repeatedly on the background of the page. What is the expected result? prompt is rejected again due to a missing user gesture. What happens instead? Install prompt is shown. Details: This page has a beforeinstallprompt event handler which immediately calls event.prompt(); essentially it is this: window.addEventListener('beforeinstallprompt', async e => { try { await e.prompt(); console.log('prompt() resolved'); } catch (ex) { console.error('prompt() rejected: ' + ex); } }); This is a legitimate thing for the website to try and do (since some user agents might show a non-modal prompt when the site calls prompt() without a user gesture). However, Chrome requires a user gesture to call prompt() and shows a modal dialogue, which is considered acceptable due to the user gesture requirement. This is a regression due to User Activation v2 (disabling #user-activation-v2 flag fixes the bug). The problem is that under UAv2, the click grants a global user gesture token for 1000 ms. If beforeinstallprompt fires during that time, the call to prompt() is allowed due to the recent click. I spelled this out in https://crbug.com/869756#c9 . Under the old system, a site that really wants to spam its users can still engage in this behaviour (by stashing the click event), but under the new system, this can easily happen by accident. I'm getting reports about this now, so opening a new bug and marking as RBS. If we really think this is the site's responsibility to not engage in this, we can close the bug out. But I am still of the opinion that changing user activation to a global state (as opposed to being a contextual token) is a mistake.
,
Dec 20
According to the spec, the BeforeInstallPrompt event is fired "when the site is allowed to present a ... prompt": https://w3c.github.io/manifest/#beforeinstallpromptevent-interface After both (a) the event has fired AND (b) the user has activated the page, why it is expected that the prompt would be rejected?
,
Dec 20
#1 Yes, this is the same issue I raised at the time. #2 > After both (a) the event has fired AND (b) the user has activated the page, why it is expected that the prompt would be rejected? Because the code that calls prompt() is NOT running in a context that was activated by a user gesture. Yes, the user has activated the page, but the prompt() call is not being done in a click handler or any context queued by the click handler. It's being called from a completely unrelated event handler (beforeinstallprompt) that was not triggered by user activation. > Tokens can't guarantee a "separation by trusted events" in JS as you noted I think I put it best in https://crbug.com/869756#c9 : "this isn't a security-based argument of "a bad site could create a bad user experience", but rather a user experience argument of "a good site could accidentally create a bad user experience."" Basically, UAv1 makes it possible to deliberately show a popup in response to an unrelated user gesture. UAv2 makes it possible to ACCIDENTALLY show a popup in response to an unrelated user gesture.
,
Dec 21
I just want to add that this is a REAL problem that we need to fix. It is not some theoretical possibility, but a real problem affecting at least one real website that we know about. This site used to work but now it shows a modal dialog unexpectedly on desktop. Please prioritize a fix for this.
,
Dec 21
I am already working on it, that's why I started looking into the spec above.
I don't think going back to the old (token-based) user activation model is the solution here, given that the new model fixes so many old problems (with Promises, postMessages etc), any of which could be used together with beforeinstallprompt.
Another reason is that I don't agree with the premise here that the prompt in the repro is "accidental" or "unexpected" when the developer explicitly makes the call. Even before UAv2, the exact same thing happens with a code that is similar+simpler than the first "usage example" in the spec [1]:
addEventListener('beforeinstallprompt', e => stashed_bip=e);
window.addEventListener('click', () => {if (stashed_bip) stashed_bip.prompt();});
I would like to investigate the failures you are facing. Let's sync offline about it after holidays.
[1] https://w3c.github.io/manifest/#ex-4-using-beforeinstallprompt-to-present-an-install-button
,
Dec 26
#5 Yes, your example code is simpler than the install button example, but my point is that your example code *explicitly* involves the site asking to cover their entire page in a click handler that shows a prompt. This isn't about what sites are technically capable of or how easy it is to implement (I've agreed with you, even back in August, that technically developers are capable of deliberately implementing the same behaviour with UAv1 or UAv2). This is about developer *intent* (are they trying to create a bad experience, or are we creating one despite their best intentions) and *predictability* (is the site behaviour reproducible or are we creating a race condition). With UAv1, you can only show a popup if you meant to do it, e.g., deliberately attaching the call to prompt() on a click handler. If you attach it to a click handler across the entire window, then you're deliberately pissing off your users, and they can close the prompt and your site. With UAv2, you can have good intentions (i.e., only show a non-modal prompt on page load if the user agent allows it) but we are creating accidentally bad behaviour. And, we've introduced a race condition, because what's happening in a background thread of execution now depends upon the user's unrelated actions in the site's UI. A site developer may not realise the behaviour because it only happens in an edge case. Happy to discuss further offline.
,
Jan 2
Friendly ping to look into this issue and to provide further update on this issue as it has been marked as a stable blocker. Thanks!
,
Jan 2
Matt, I agree that there's a fundamental tradeoff here in the user activation model. UAv2 indeed makes it easier to trigger some annoying behavior the user agent was previously protecting against. Even bigger than the install prompt is pop-ups. I think all your arguments apply equally well to sites which are doing "window.open" during page load - they could rely today on the browsers pop-up blocker preventing user annoyance. The judgement of blink leads (at least Ojan and I) is that the long-term benefits of this model (especially being simple enough to be potentially fully interoperable) outweigh this cost. But it's a judgement call and of course we might be wrong. But I think we have to weigh this trade-off universally, not just specifically for beforeinstallprompt (I assume window.open is still orders of magnitude more significant a use case than the install prompt, right?). We can re-open the debate about whether UAv2 is really better than UAv1 in general, but I think we should do that scientifically with new hard data, not just the same old opinions. I've always considered shipping UAv2 to be necessary to getting such hard data, but that's certainly debatable. Separately I think there is a legitimate web compat question here. Even if we agreed UAv2 was the right activation model, perhaps the change in behavior for beforeinstallprompt specifically will impose enough of a compat problem that it fails to meet the bit.ly/blink-compat bar for shipping without some additional mitigation (eg. maybe we need to do outreach to one or two popular sites before launching). We could discuss this on the intent thread, but we'd want some hard data on metrics. If the argument is that users will be annoyed by more prompts, then we should be able to quantify that by looking at metrics like: 1) what fraction of prompt() calls result in an actual prompt 2) what fraction of shown prompts are accepted If I'm reading the AppBanner UMA metrics correctly (go/m72-uma-install-banner) on Android M72 has about 70% more install prompts shown, but almost exactly the same accept rate. On Windows it looks like a 5x greater prompt rate (!) with maybe 10% lower accept rate. Do you agree with that analysis of the data? How significant would you say that is qualitatively? Do we have any way (eg. UKM) to quantify the extent to which this is dominated by a few sites we could expect to update their code? Of course we also need to look at the absolute numbers (but perhaps can't share them in this public forum) to weigh overall magnitude. Do we have any more anecdotal data, eg. how many users have complained, how many different sites have they discussed? One option to consider adding some sort of BeforeInstallPromptEvent-specific additional logic. Eg. perhaps the spec should say that if the prompt will be modal, then it MUST reject if not called from within the context of a 'click' event handler whose isTrusted is true? This would still avoid all the complexity of generic user gestures, but might cause new compat problems with eliminating prompts that previously worked.
,
Jan 3
> The judgement of blink leads (at > least Ojan and I) is that the long-term benefits of this model (especially > being simple enough to be potentially fully interoperable) outweigh this cost. I don't know all the details, but my feeling on this has always been that the old model was more complex than the new one because it is "better" (more predictably ties user gestures to the APIs that consume them, whereas the new model has inherent race conditions built in). There is more complexity there, but it feels like necessary complexity. And the old model had bugs — I know several of my clients (of Web Share) ran into a bug where the user gesture token was being lost when passed through an XHR. But that doesn't mean it's not standardizable, and those bugs could be fixed instead of replacing it with a simpler model. > But it's a judgement call and of course we might be wrong. But I think we have > to weigh this trade-off universally, not just specifically for > beforeinstallprompt (I assume window.open is still orders of magnitude more > significant a use case than the install prompt, right?). Yes. I think the BIP case came up more because we had previously *actively encouraged* developers to call prompt() as soon as they got the event, when we removed the automatic prompting. The reasoning was that that gets you back to the old behaviour: if the browser supports passive (non-modal) prompting, then the prompt would be shown non-intrusively and right away; if the browser supports modal prompting but gates on a user gesture, then the call to prompt() would fail harmlessly. That latter assumption has broken with UAv2. > Separately I think there is a legitimate web compat question here. Even if we > agreed UAv2 was the right activation model, perhaps the change in behavior for > beforeinstallprompt specifically will impose enough of a compat problem that > it fails to meet the bit.ly/blink-compat bar for shipping without some > additional mitigation (eg. maybe we need to do outreach to one or two popular > sites before launching). We could discuss this on the intent thread, but we'd > want some hard data on metrics. If the argument is that users will be annoyed > by more prompts, then we should be able to quantify that by looking at metrics > like: > 1) what fraction of prompt() calls result in an actual prompt > 2) what fraction of shown prompts are accepted I don't really think of this as a quantitative question. It isn't that users are going to be annoyed by "more prompts". It's that sites are unknowingly presenting users with a bad experience (basically, I'm thinking this is bad for developers more than bad for users). We can do outreach to make sure sites aren't doing this (I don't know of any major PWAs that do). Maybe we just need to give sites advice in the future not to call prompt() in their handler, and then the specific problem to BIP will go away. I have broader concerns, though, about this model, that it basically makes it very hard to predict when calls to a user-gesture-requiring API will succeed, due to the inherent cross-communication between concurrent threads of execution. The old model had one predictability problem (the timeout). The new model introduces a much harder one, which is that an API call in one thread of execution can now consume the user gesture token created by another. Here's an example I cooked up (see attached HTML file). This page has two buttons, which both create a popup window after a 500 ms timer (that simulates some asynchronous operation that takes a bit of time). With UAv1, if you click both buttons quickly (with <500ms gap in between), both popup windows will open, since each call to window.open was accompanied by a corresponding user gesture. 1 gesture = 1 popup. With UAv2, the second call to window.open will be popup-blocked, since the first call consumed the global user activation token. This concept doesn't just apply to popup opening. Any two unrelated pieces of code could unintentionally grant or consume each other's user gestures, because user gesture tokens are no longer associated with a particular context. > One option to consider adding some sort of BeforeInstallPromptEvent-specific > additional logic. Eg. perhaps the spec should say that if the prompt will be > modal, then it MUST reject if not called from within the context of a 'click' > event handler whose isTrusted is true? This would still avoid all the > complexity of generic user gestures, but might cause new compat problems with > eliminating prompts that previously worked. We don't currently have this requirement in the spec, and you're right, I think the spec should have an opinion about this (though we probably can't normatively say "modal", we can have a MAY with some non-normative discussion around modal vs non-modal dialog). But that's a spec issue. Let's assume we do put that text in the spec (which is effectively the unwritten text we're implementing in Chrome). What you're essentially asking for "it MUST reject if not called from within the context of a 'click' event handler whose isTrusted is true" is the UAv1 semantics, isn't it? The idea of "the context of a click event handler" is being removed with UAv2. There is no more concept of "this code is in the context of a click event handler"; now all we have is "has the user clicked anywhere in the past 1000ms". Are you suggesting that BIP retain the UAv1 semantics, while everything else moves to UAv2? That doesn't solve the general issues (such as for window.open).
,
Jan 4
I had a sync offline with mustaq@ and dominickn@. I still have issues with the UAv2 model (but mustaq@ explained more of the issues with the old model, which I accept). From the perspective of BIP, however, I don't intend to block UAv2, nor do I think there's anything really to do on our end. There isn't really anything special about BIP here* compared to other gesture-requiring APIs. All gesture-requiring APIs can now be called from a non-user-input event (such as page load, or a long timeout), and if they are, the new model creates a race condition where the call will succeed if the user had recently clicked on the page. If the web platform team is aware of that model and accepts it, then I think this BIP bug is WAI and we close this out. The way I've been able to come to terms with this model is to accept that: a) UAv2 does not enable a malicious site to do anything that UAv1 didn't allow, AND b) The UA system is not designed to help a legit site avoid shooting themselves in the foot (it protects users from sites, not sites from themselves). If you ask for BIP.prompt() or window.open() when you shouldn't, and it sometimes "happens to work", then you're asking for a bad user experience and sometimes getting it. (Though I still think that having a platform that seems to work, except in edge cases where it goes bad, is not a good design.) The solution is for developers to not do that. (In this specific case, we would tell developers to not call prompt() inside the beforeinstallprompt event handler; always stash it and wait for a click.) Fortunately, ALL of the tutorials I can find on this event tell you to do that, so I have no reason to believe this bad behaviour is widespread. Thus, I'm closing this out. *The one counfounding factor that applies to BIP but not other APIs is the historical usage of it without requiring a user gesture. That means there may be sites out there that attempt to call BIP.prompt() without a user gesture which previously did not ever show a prompt, and now will racily show a prompt. However, under the new model, we simply say that that's the developer creating a bad experience, and they have a clear path forward: don't call BIP outside of a user input event handler.
,
Jan 4
The issue I reported in #9 copied out to a separate bug, with some thoughts on how to solve it under UAv2. Issue 919017.
,
Jan 4
|
|||
►
Sign in to add a comment |
|||
Comment 1 by mustaq@chromium.org
, Dec 20