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

Remove experimental flag from visibility triggers and add documentation. #3512

Merged
merged 4 commits into from
Jun 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions extensions/amp-analytics/0.1/instrumentation.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* limitations under the License.
*/

import {isExperimentOn} from '../../../src/experiments';
import {isVisibilitySpecValid} from './visibility-impl';
import {Observable} from '../../../src/observable';
import {getService} from '../../../src/service';
Expand Down Expand Up @@ -213,7 +212,7 @@ export class InstrumentationService {
* @private
*/
createVisibilityListener_(callback, config) {
if (config['visibilitySpec'] && this.isViewabilityExperimentOn_()) {
if (config['visibilitySpec']) {
if (!isVisibilitySpecValid(config)) {
return;
}
Expand Down Expand Up @@ -456,13 +455,6 @@ export class InstrumentationService {
this.win_.setTimeout(this.win_.clearInterval.bind(this.win_, intervalId),
maxTimerLength * 1000);
}

/**
* @return {boolean} True if the experiment is on. False otherwise.
*/
isViewabilityExperimentOn_() {
return isExperimentOn(this.win_, 'amp-analytics-viewability');
}
}

/**
Expand Down
106 changes: 87 additions & 19 deletions extensions/amp-analytics/0.1/test/test-visibility-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,25 @@
* limitations under the License.
*/

import {adopt} from '../../../../src/runtime';
import {createIframePromise} from '../../../../testing/iframe';
import {
isPositiveNumber_,
isValidPercentage_,
isVisibilitySpecValid,
installVisibilityService,
} from '../visibility-impl';
import {installResourcesService} from '../../../../src/service/resources-impl';
import {installViewerService} from '../../../../src/service/viewer-impl';
import {installViewportService} from '../../../../src/service/viewport-impl';
import {layoutRectLtwh, rectIntersection} from '../../../../src/layout-rect';
import {visibilityFor} from '../../../../src/visibility';
import {VisibilityState} from '../../../../src/visibility-state';
import * as sinon from 'sinon';


adopt(window);

// The tests have amp-analytics tag because they should be run whenever
// amp-analytics is changed.
describe('Visibility (tag: amp-analytics)', () => {
Expand All @@ -34,10 +41,11 @@ describe('Visibility (tag: amp-analytics)', () => {
let visibility;
let getIntersectionStub;
let callbackStub;
let win;

const INTERSECTION_0P = makeIntersectionEntry([100, 100, 100, 100],
[0, 0, 100, 100]);
const INTERSECTION_1P = makeIntersectionEntry([99, 99, 100, 100],
const INTERSECTION_1P = makeIntersectionEntry([90, 90, 100, 100],
[0, 0, 100, 100]);
const INTERSECTION_50P = makeIntersectionEntry([50, 0, 100, 100],
[0, 0, 100, 100]);
Expand All @@ -46,56 +54,63 @@ describe('Visibility (tag: amp-analytics)', () => {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();

installResourcesService(window);
installVisibilityService(window);

const getIdStub = sandbox.stub();
getIdStub.returns('0');
getIntersectionStub = sandbox.stub();
callbackStub = sandbox.stub();

return visibilityFor(window).then(v => {
visibility = v;
const getResourceStub = sandbox.stub(visibility.resourcesService_,
'getResourceForElement');
getResourceStub.returns({
element: {getIntersectionChangeEntry: getIntersectionStub},
getId: getIdStub,
isLayoutPending: () => false,
return createIframePromise().then(iframe => {
installViewerService(iframe.win);
installViewportService(iframe.win);
installResourcesService(iframe.win);
installVisibilityService(iframe.win);

return visibilityFor(iframe.win).then(v => {
visibility = v;
sandbox.stub(visibility.resourcesService_,
'getResourceForElement').returns({
element: {getIntersectionChangeEntry: getIntersectionStub},
getId: getIdStub,
isLayoutPending: () => false});
});
});
});

afterEach(() => {
visibility = null;
getIntersectionStub = null;
callbackStub = null;;
sandbox.restore();
});

function makeIntersectionEntry(boundingClientRect, rootBounds) {
boundingClientRect = layoutRectLtwh.apply(window, boundingClientRect);
rootBounds = layoutRectLtwh.apply(window, rootBounds);
boundingClientRect = layoutRectLtwh.apply(win, boundingClientRect);
rootBounds = layoutRectLtwh.apply(win, rootBounds);
return {
intersectionRect: rectIntersection(boundingClientRect, rootBounds),
boundingClientRect,
rootBounds,
};
}

function listen(intersectionChange, config, expectedCalls) {
function listen(intersectionChange, config, expectedCalls, opt_expectedVars) {
getIntersectionStub.returns(intersectionChange);
config['selector'] = '#abc';
sandbox.clock.tick(0);
visibility.listenOnce(config, callbackStub);
sandbox.clock.tick(20);
expect(callbackStub.callCount).to.equal(expectedCalls);
if (opt_expectedVars && expectedCalls > 0) {
for (let c = 0; c < opt_expectedVars.length; c++) {
sinon.assert.calledWith(callbackStub.getCall(c), opt_expectedVars[c]);
}
}
}

function verifyChange(intersectionChange, expectedCalls, opt_expectedVars) {
getIntersectionStub.returns(intersectionChange);
visibility.scrollListener_();
expect(callbackStub.callCount).to.equal(expectedCalls);
if (opt_expectedVars) {
if (opt_expectedVars && expectedCalls > 0) {
for (let c = 0; c < opt_expectedVars.length; c++) {
sinon.assert.calledWith(callbackStub.getCall(c), opt_expectedVars[c]);
}
Expand All @@ -118,6 +133,7 @@ describe('Visibility (tag: amp-analytics)', () => {
elementY: '0',
elementWidth: '100',
elementHeight: '100',
loadTimeVisibility: '50',
totalTime: sinon.match(value => {
return !isNaN(Number(value));
}),
Expand Down Expand Up @@ -168,7 +184,7 @@ describe('Visibility (tag: amp-analytics)', () => {
// There is a 20ms offset in some timedurations because of initial
// timeout in the listenOnce logic.
sinon.assert.calledWith(callbackStub.getCall(0), sinon.match({
maxContinuousTime: '1000',
maxContinuousVisibleTime: '1000',
totalVisibleTime: '1000',
firstSeenTime: '20',
fistVisibleTime: '1020',
Expand Down Expand Up @@ -204,13 +220,65 @@ describe('Visibility (tag: amp-analytics)', () => {
sandbox.clock.tick(900);
expect(callbackStub.callCount).to.equal(1);
sinon.assert.calledWith(callbackStub.getCall(0), sinon.match({
maxContinuousTime: '1000',
maxContinuousVisibleTime: '1000',
minVisiblePercentage: '50',
maxVisiblePercentage: '50',
totalVisibleTime: '1999',
}));
});

it('populates backgroundedAtStart=1', () => {
visibility.backgroundedAtStart_ = true;
listen(INTERSECTION_50P, {
visiblePercentageMin: 0, visiblePercentageMax: 100}, 1, [sinon.match({
'backgroundedAtStart': '1',
})]);
});

it('populates backgroundedAtStart=0', () => {
const viewerStub = sandbox.stub(visibility.viewer_, 'getVisibilityState');
visibility.backgroundedAtStart_ = false;
listen(INTERSECTION_50P, {
visiblePercentageMin: 0, visiblePercentageMax: 100}, 1, [sinon.match({
'backgroundedAtStart': '0',
'backgrounded': '0',
})]);

viewerStub.returns(VisibilityState.HIDDEN);
visibility.visibilityListener_();
listen(INTERSECTION_50P, {
visiblePercentageMin: 0, visiblePercentageMax: 100}, 2, [
sinon.match({}),
sinon.match({
'backgroundedAtStart': '0',
'backgrounded': '1',
})]);
});

describe('populates backgrounded variable', () => {
let viewerStub;
beforeEach(() => {
viewerStub = sandbox.stub(visibility.viewer_, 'getVisibilityState');
});

function verifyState(state, expectedValue) {
it('for visibility state=' + state, () => {
viewerStub.returns(state);
visibility.visibilityListener_();

listen(INTERSECTION_50P, {
visiblePercentageMin: 0, visiblePercentageMax: 100}, 1, [sinon.match({
'backgrounded': expectedValue,
})]);
});
}

verifyState(VisibilityState.VISIBLE, '0');
verifyState(VisibilityState.HIDDEN, '1');
verifyState(VisibilityState.PAUSED, '1');
verifyState(VisibilityState.INACTIVE, '1');
});

describe('isVisibilitySpecValid', () => {
it('passes valid visibility spec', () => {
const specs = [
Expand Down
2 changes: 1 addition & 1 deletion extensions/amp-analytics/0.1/visibility-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {VisibilityState} from '../../../src/visibility-state';
const LISTENER_INITIAL_RUN_DELAY_ = 20;

// Variables that are passed to the callback.
const MAX_CONTINUOUS_TIME = 'maxContinuousTime';
const MAX_CONTINUOUS_TIME = 'maxContinuousVisibleTime';
const TOTAL_VISIBLE_TIME = 'totalVisibleTime';
const FIRST_SEEN_TIME = 'firstSeenTime';
const LAST_SEEN_TIME = 'lastSeenTime';
Expand Down
18 changes: 15 additions & 3 deletions extensions/amp-analytics/amp-analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,13 +319,25 @@ The `triggers` attribute describes when an analytics request should be sent. It
- `timerSpec` (required when `on` is set to `timer`) This configuration is used on conjunction with the `timer` trigger. Please see below for details.

#### Page visible trigger (`"on": "visible"`)
Use this configuration to fire a request when the page becomes visible. No further configuration is required.
Use this configuration to fire a request when the page becomes visible. The firing of this trigger can be configured using `visibilitySpec`. If multiple properties are specified, they must all be true in order for a request to fire. Configuration properties supported in `visibilitySpec` are:
- `selector` This property can be used to specify the element to which all the `visibilitySpec` conditions apply.
- `continuousTimeMin` and `continuousTimeMax` These properties indicate that a request should be fired when (any part of) an element has been within the viewport for a continuous amount of time that is between the minimum and maximum specified times. The times are expressed in milliseconds.
- `totalTimeMin` and `totalTimeMax` These properties indicate that a request should be fired when (any part of) an element has been within the viewport for a total amount of time that is between the minimum and maximum specified times. The times are expressed in milliseconds.
- `visiblePercentageMin` and `visiblePercentageMax` These properties indicate that a request should be fired when the proportion of an element that is visible within the viewport is between the minimum and maximum specified percentages. Percentage values between 0 and 100 are valid. Note that the lower bound (`visiblePercentageMin`) is inclusive while the upper bound (`visiblePercentageMax`) is not. When these properties are defined along with other timing related properties, only the time when these properties are met are counted.

In addition to the conditions above, `visibilitySpec` also enables certain variables which are documented [here](./analytics-vars.md#visibility-variables).
Copy link
Contributor

Choose a reason for hiding this comment

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

Assuming the statement I write here is true, should have something like: "If multiple properties are specified, they must all be true in order for a request to fire."

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.


```javascript
"triggers": {
"defaultPageview": {
"on": "visible",
"request": "pageview"
"request": "pageview",
"visibilitySpec": {
"selector": "#anim-id",
"visiblePercentageMin": 20,
"totalTimeMin": 500,
"continuousTimeMin": 200
}
}
}
```
Expand All @@ -348,7 +360,7 @@ Use this configuration to fire a request when a specified element is clicked. Us
```

#### Scroll trigger (`"on": "scroll"`)
Use this configuration to fire a request under certain conditions when the page is scrolled. Use `scrollSpec` to control when this will fire:
Use this configuration to fire a request under certain conditions when the page is scrolled. This trigger provides [special vars](./analytics-vars.md#interaction) that indicate the boundaries that triggered a request to be sent. Use `scrollSpec` to control when this will fire:
- `scrollSpec` This object can contain `verticalBoundaries` and `horizontalBoundaries`. At least one of the two properties is required for a scroll event to fire. The values for both of the properties should be arrays of numbers containing the boundaries on which a scroll event is generated. For instance, in the following code snippet, the scroll event will be fired when page is scrolled vertically by 25%, 50% and 90%. Additionally, the event will also fire when the page is horizontally scrolled to 90% of scroll width. To keep the page performant, the scroll boundaries are rounded to the nearest multiple of `5`.


Expand Down
82 changes: 82 additions & 0 deletions extensions/amp-analytics/analytics-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ When the same `var` is defined in multiple locations, the value is picked in the

The remainder of this doc lists and describes the variables supported in `amp-analytics`.

| Table of Contents |
|---|
| [Device and Browser](#device-and-browser) |
| [Interaction](#interaction) |
| [Miscellaneous](#miscellaneous) |
| [Page and content](#page-and-content) |
| [Performance](#performance) |
| [Visibility](#visibility-variables) |


## Page and content

### ampdocHost
Expand Down Expand Up @@ -361,3 +371,75 @@ Provides the number of seconds that have elapsed since 1970. (Epoch time)

Example value: `1452710304312`

## Visibility Variables

### backgrounded
Copy link
Contributor

Choose a reason for hiding this comment

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

Are the below two variables related to prerendering or separate. It might be good to note either way. For example, when could a page be backgroundedAtStart?

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 is not related to prerendering. Nothing in amp-analytics executes before the page is made visible afaik. Will add some context here.


A binary variable with possible values of 1 and 0 to indicate that the page/tab was sent to background at any point before the hit was sent. 1 indicates that the page was backgrounded while 0 indicates that the page has always been in the foreground. This variable does not count prerender as a backgrounded state.

### backgroundedAtStart

A binary variable with possible values of 1 and 0 to indicate that the page/tab was backgrounded at the time when the page was loaded. 1 indicates that the page was loaded in the background while 0 indicates otherwise. This variable does not count prerender as a backgrounded state.

### maxContinuousVisibleTime

Provides the maximum amount of continuous time an element has met the `visibilitySpec` conditions at the time this ping is sent. Note that a ping with a continuousTimeMin=1000 and totalTimeMin=5000 that is visible for 1000ms, then not visible, then visible
for 2000ms, then not, then visible for 1000ms, then not, then visible for 1020ms
will report 2000 for this number as that is the max continuous visible time,
even if it is not the current continuous visible time (1020 in this example).
Copy link
Contributor

Choose a reason for hiding this comment

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

Would currentContinuousVisible time be useful to provide? Seems like it might be.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably. Can you open an issue to track it?


### elementHeight

Provides the height of the element specified by `visibilitySpec`.

### elementWidth

Provides the width of the element specified by `visibilitySpec`.

### elementX

Provides the X coordinate of the left edge of the element specified by `visibilitySpec`.

### elementY

Provides the Y coordinate of the top edge of the element specified by `visibilitySpec`.

### firstSeenTime

Provides the time when at least 1px of the element is on the screen for the first time since the trigger is registered by `amp-analytics`.

### firstVisibleTime

Provides the time when the element met visibility conditions for the first time since
the trigger is registered by `amp-analytics`.

### lastSeenTime

Provides the time when at least 1px of the element is on the screen for the last time since javascript load.

### lastVisibleTime

Provides the time when the element met visibility conditions for the last time since
javascript load.

### loadTimeVisibility

Provides the percentage of element visible in the viewport at load time. This variable assumes that the page is scrolled to top.

### maxVisiblePercentage

Provides the maximum visible percentage over the time that `visibilitySpec` conditions were met. For example, a ping where the element was 100%, then off the page, then 100% will report this value as 100. A ping with visiblePercentageMax=50 undergoing the same transitions would report somewhere between 0 and 50 since any time when the element was 100% on the page would not be counted.

### minVisiblePercentage

Provides the minimum visible percentage over the time that visibilitySpec conditions were met. For example, a ping where the element was 100%, then off the page, then 100% will report this value as 0. A ping with visiblePercentageMin=50 condition undergoing the same transitions would report somewhere between 50 and 100 since any time when the element was 0% to 50% on the page would not be counted.

### totalTime

Provides the total time from the time page was loaded to the time a ping was sent out. The value is calculated from the time document became interactive.

### totalVisibleTime

Provides the total time for which the element has met the visiblitySpec conditions at time this ping is sent.


5 changes: 0 additions & 5 deletions tools/experiments/experiments.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,6 @@ const EXPERIMENTS = [
spec: 'https://github.com/ampproject/amphtml/blob/master/' +
'extensions/amp-fx-flying-carpet/amp-fx-flying-carpet.md',
},
{
id: 'amp-analytics-viewability',
name: 'Viewability APIs for amp-analytics',
spec: 'https://github.com/ampproject/amphtml/issues/1297#issuecomment-197441289',
},
{
id: 'amp-sticky-ad',
name: 'AMP Sticky Ad',
Expand Down