Skip to content

Commit

Permalink
✨Create new extension - AMP-smartlinks (ampproject#20967)
Browse files Browse the repository at this point in the history
* creating new amp-smartlinks extension

* BAM-2584 AMP-Smartlinks (ampproject#3)

* adding example page and amp-smartlinks to bundles.config

* creating amp-smartlinks scaffolding

* BAM-2584 adding hardcoded POC implementation of amp-smartlinks

* adding new constants file and linkmate file/class

* adding linkmate call and association working with link-rewrite service

* add amp config call and update workflow to use those values

* setup initial amp-smartlinks and linkmate workflow

* adding new config variables for link attribute and selector

* add tests for amp-smartlinks

* add test for linkmate.js

* add more thorough anchorList check in runSmartlinks

* adding code comments, new constants structure and better options validation

* update amp-smartlinks to have helpful information

* updating global vars and cleaning up main files

* clean up tests and add more explicit type assertions

* clean up jsdoc tags

* BAM-2585 Whitelist import, fix type errors, and replace user.assert with userAssert (ampproject#4)

* BAM-2585 whitelist navigation import and fix type errors

* replace user.assert with userAssert

* BAM-2585 fix validator and type check (ampproject#5)

* more validator fixes (ampproject#6)

* BAM-2585 Fix validator, copyright, and whitespace (ampproject#7)

* alphabetize validator, fix whitespace, and add valid tag

* update year in copyright statement

* BAM-2585 move `link-rewriter` to import statements and updating types (ampproject#8)

* BAM-2585 remove link-rewriter and switch to importing from skimlinks extension

* clean up promise chain and more descriptive API comments

* update xhr to pull from ampdoc.win and add types to constants.js

* fix type in buildPageImpressionPayload_

* update validator with new empty value check (ampproject#9)

* update validator for exclusive-links

* switch page-impression API request to customEventReporter (ampproject#11)

* BAM-2585 fix jsdoc in linkmate-options and unnecessary param in page_impression request (ampproject#12)

* fix jsdoc for linkmate params and make runLinkmate more readable (ampproject#13)

* add try/catch on amp_config fetch and updated constants.js

* remove bad type in constants.js and add check for existing shop-links (ampproject#14)

* add check for auction_id in mapLinks and add jsdoc for SMARTLINKS_REWRITER_ID

* fix type notation in constants.js and linkmate-options.js

* fix indentation in example file

* add note to README describing link-rewriter priority queue behavior (ampproject#15)

* add exception to compile.js for amp-skimlinks

* update validator to use empty value as indicator linkmate param

* fix validator and linkmate-options to use new config style (ampproject#16)

* updating tests for linkmate-options.js

* remove redundant userAssert in linkmate-options

* update tests to reflect config changes

* update tests to send accurate config params

* update readme to refelct config changes and more accurate function names in linkmate-options
  • Loading branch information
PhilWinchester authored and bramanudom committed Mar 22, 2019
1 parent c268e40 commit 2959313
Show file tree
Hide file tree
Showing 17 changed files with 1,503 additions and 0 deletions.
4 changes: 4 additions & 0 deletions build-system/dep-check-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ exports.rules = [
'extensions/amp-subscriptions-google/0.1/amp-subscriptions-google.js->extensions/amp-subscriptions/0.1/doc-impl.js',
'extensions/amp-subscriptions-google/0.1/amp-subscriptions-google.js->extensions/amp-subscriptions/0.1/entitlement.js',
'extensions/amp-subscriptions-google/0.1/amp-subscriptions-google.js->extensions/amp-subscriptions/0.1/score-factors.js',

// amp-smartlinks depends on amp-skimlinks/link-rewriter
'extensions/amp-smartlinks/0.1/amp-smartlinks.js->extensions/amp-skimlinks/0.1/link-rewriter/link-rewriter-manager.js',
'extensions/amp-smartlinks/0.1/linkmate.js->extensions/amp-skimlinks/0.1/link-rewriter/two-steps-response.js',
],
},
{
Expand Down
2 changes: 2 additions & 0 deletions build-system/tasks/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ function compile(entryModuleFilenames, outputDir, outputFilename, options) {
'extensions/amp-viewer-assistance/**/*.js',
// Needed for AmpViewerIntegrationVariableService
'extensions/amp-viewer-integration/**/*.js',
// Needed for amp-smartlinks dep on amp-skimlinks
'extensions/amp-skimlinks/0.1/**/*.js',
'src/*.js',
'src/**/*.js',
'!third_party/babel/custom-babel-helpers.js',
Expand Down
1 change: 1 addition & 0 deletions bundles.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ exports.extensionBundles = [
type: TYPES.MISC,
},
{name: 'amp-skimlinks', version: '0.1', type: TYPES.MISC},
{name: 'amp-smartlinks', version: '0.1', type: TYPES.MISC},
{name: 'amp-soundcloud', version: '0.1', type: TYPES.MEDIA},
{name: 'amp-springboard-player', version: '0.1', type: TYPES.MEDIA},
{
Expand Down
47 changes: 47 additions & 0 deletions examples/amp-smartlinks.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!--
Copyright 2019 The AMP HTML Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS-IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the license.
-->
<!--
Test Description:
Valid amp-smartlinks tag
-->
<!doctype html>
<html >
<head>
<meta charset="utf-8">
<link rel="canonical" href="./regular-html-version.html">
<meta name="viewport" content="width=device-width,minimum-scale=1">
<meta name="amp-link-rewriter-priorities" content="amp-smartlinks">
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<script async custom-element="amp-smartlinks" src="https://cdn.ampproject.org/v0/amp-smartlinks-0.1.js"></script>
<script async src="https://cdn.ampproject.org/v0.js"></script>
</head>
<body>
<title>AMP Smartlinks</title>
<h1>Hello from Narrativ</h1>
<amp-smartlinks
layout="nodisplay"
nrtv-account-name="amppublisher"
linkmate
link-selector="a">
</amp-smartlinks>

<div class="linkmate-eligible-links">
<a href="https://www.exampleretailer.com/prod1234">Example Retailer</a>
<a href="https://www.exampleretailer.com/prod1234">Duplicate Example Retailer</a>
</div>

<div class="linkmate-ineligible-links">
<a href="https://www.badretailer.com/worstProductEver4321">Bad Retailer</a>
</div>
</body>
</html>
4 changes: 4 additions & 0 deletions extensions/amp-smartlinks/0.1/OWNERS.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- PhilWinchester
- pbecotte
- c-nichols
- zhouyx
220 changes: 220 additions & 0 deletions extensions/amp-smartlinks/0.1/amp-smartlinks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/**
* Copyright 2019 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {CommonSignals} from '../../../src/common-signals';
import {CustomEventReporterBuilder} from '../../../src/extension-analytics.js';
import {Services} from '../../../src/services';
import {dict} from '../../../src/utils/object';
import {getData} from './../../../src/event-helper';

import {ENDPOINTS} from './constants';
import {LinkRewriterManager} from
'../../amp-skimlinks/0.1/link-rewriter/link-rewriter-manager';
import {Linkmate} from './linkmate';
import {getConfigOptions} from './linkmate-options';

const TAG = 'amp-smartlinks';


export class AmpSmartlinks extends AMP.BaseElement {
/**
* @param {!AmpElement} element
*/
constructor(element) {
super(element);

/** @private {?../../../src/service/xhr-impl.Xhr} */
this.xhr_ = null;

/** @private {?../../../src/service/ampdoc-impl.AmpDoc} */
this.ampDoc_ = null;

/** @private {?../../amp-skimlinks/0.1/link-rewriter/link-rewriter-manager.LinkRewriterManager} */
this.linkRewriterService_ = null;

/** @private {?../../amp-skimlinks/0.1/link-rewriter/link-rewriter.LinkRewriter} */
this.smartLinkRewriter_ = null;

/**
* This will store config attributes from the extension options and an API
* request. The attributes from options are:
* exclusiveLinks, linkAttribute, linkSelector, linkmateEnabled, nrtvSlug
* The attributes from the API are:
* linkmateExpected, publisherID
* @private {?Object} */
this.linkmateOptions_ = null;

/** @private {?./linkmate.Linkmate} */
this.linkmate_ = null;

/** @private {?string} */
this.referrer_ = null;
}

/** @override */
buildCallback() {
this.ampDoc_ = this.getAmpDoc();
this.xhr_ = Services.xhrFor(this.ampDoc_.win);
const viewer = Services.viewerForDoc(this.ampDoc_);

this.linkmateOptions_ = getConfigOptions(this.element);
this.linkRewriterService_ = new LinkRewriterManager(this.ampDoc_);

return this.ampDoc_.whenBodyAvailable()
.then(() => viewer.getReferrerUrl())
.then(referrer => {
this.referrer_ = referrer;
viewer.whenFirstVisible().then(() => {
this.runSmartlinks_();
});
});
}

/**
* Wait for the config promise to resolve and then proceed to functionality
* @private
*/
runSmartlinks_() {
this.getLinkmateOptions_().then(config => {
this.linkmateOptions_.linkmateExpected = config['linkmate_enabled'];
this.linkmateOptions_.publisherID = config['publisher_id'];

this.postPageImpression_();
this.linkmate_ = new Linkmate(
/** @type {!../../../src/service/ampdoc-impl.AmpDoc} */
(this.ampDoc_),
/** @type {!../../../src/service/xhr-impl.Xhr} */
(this.xhr_),
/** @type {!Object} */
(this.linkmateOptions_)
);
this.smartLinkRewriter_ = this.initLinkRewriter_();

// If the config specified linkmate to run and our API is expecting
// linkmate to run
if (this.linkmateOptions_.linkmateEnabled &&
this.linkmateOptions_.linkmateExpected) {
this.smartLinkRewriter_.getAnchorReplacementList();
}
});
}

/**
* API call to retrieve the Narrativ config for this extension.
* API response will be a list containing nested json values. For the purpose
* of this extension there will only ever be one value in the list:
* {amp_config: {linkmate_enabled: <!boolean>, publisher_id: <!number>}}
* @return {?Promise<!JsonObject>}
* @private
*/
getLinkmateOptions_() {
const fetchUrl = ENDPOINTS.NRTV_CONFIG_ENDPOINT.replace(
'.nrtv_slug.', this.linkmateOptions_.nrtvSlug
);

try {
return this.xhr_.fetchJson(fetchUrl, {
method: 'GET',
ampCors: false,
})
.then(res => res.json())
.then(res => {
return getData(res)[0]['amp_config'];
});
} catch (err) {
return null;
}
}


/**
* API call to indicate a page load event happened
* @private
*/
postPageImpression_() {
// When using layout='nodisplay' manually trigger CustomEventReporterBuilder
this.signals().signal(CommonSignals.LOAD_START);
const payload = this.buildPageImpressionPayload_();

const builder = new CustomEventReporterBuilder(this.element);

builder.track('page-impression', ENDPOINTS.PAGE_IMPRESSION_ENDPOINT);

builder.setTransportConfig(dict({
'beacon': true,
'image': false,
'xhrpost': true,
'useBody': true,
}));

builder.setExtraUrlParams(payload);
const reporter = builder.build();

reporter.trigger('page-impression');
}

/**
* Initialize and register a Narrativ LinkRewriter instance
* @return {!../../amp-skimlinks/0.1/link-rewriter/link-rewriter.LinkRewriter}
* @private
*/
initLinkRewriter_() {
const options = {linkSelector: this.linkmateOptions_.linkSelector};

return this.linkRewriterService_.registerLinkRewriter(
TAG,
anchorList => {
return this.linkmate_.runLinkmate(anchorList);
},
options
);
}

/**
* Build the payload for our page load event.
* @return {!JsonObject}
* @private
*/
buildPageImpressionPayload_() {
return /** @type {!JsonObject} */ (dict({
'events': [{'is_amp': true}],
'organization_id': this.linkmateOptions_.publisherID,
'organization_type': 'publisher',
'user': {
'page_session_uuid': this.generateUUID_(),
'source_url': this.ampDoc_.getUrl(),
'previous_url': this.referrer_,
'user_agent': this.ampDoc_.win.navigator.userAgent,
},
}));
}

/**
* Generate a unique UUID for this session.
* @return {string}
* @private
*/
generateUUID_() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4)
.toString(16)
);
}
}

AMP.extension('amp-smartlinks', '0.1', AMP => {
AMP.registerElement('amp-smartlinks', AmpSmartlinks);
});
26 changes: 26 additions & 0 deletions extensions/amp-smartlinks/0.1/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright 2019 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const BASE_API_URL = 'https://api.narrativ.com/api';
/** @const @enum {string} */
export const ENDPOINTS = {
PAGE_IMPRESSION_ENDPOINT:
`${BASE_API_URL}/v1/events/impressions/page_impression/`,
NRTV_CONFIG_ENDPOINT:
`${BASE_API_URL}/v0/publishers/.nrtv_slug./amp_config/`,
LINKMATE_ENDPOINT:
`${BASE_API_URL}/v1/publishers/.pub_id./linkmate/smart_links/`,
};
Loading

0 comments on commit 2959313

Please sign in to comment.