Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
675 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules | ||
.idea | ||
test.tmp.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,75 @@ | ||
# Google AMP CID API Library | ||
# Google AMP Client ID Library | ||
|
||
## Introduction | ||
TODO: add introduction | ||
|
||
## Usage | ||
|
||
##### Use the compiled binary served on Google CDN | ||
Add the script tag into your HTML header. | ||
```html | ||
<head> | ||
... | ||
<script async src="https://ampcid.google.com/googleampcidapi.js"></script> | ||
</head> | ||
``` | ||
|
||
In your Javascript code, wait for the library load and use it: | ||
```js | ||
function ampCidApiOnload(callback) { | ||
(self.googleAmpCidApiOnload = self.googleAmpCidApiOnload || []).push(callback); | ||
} | ||
|
||
ampCidApiOnload(function(api) { | ||
api.getScopedCid('scope-abc', 'YOUR_API_KEY', function(err, cid) { | ||
if (err) { | ||
alert('Error: ' + err); | ||
} else { | ||
alert('Client ID:' + cid); | ||
} | ||
}); | ||
}); | ||
``` | ||
|
||
##### Copy the code into your own project | ||
Using the binary served from CDN is the recommended way, which saves you from | ||
version update. | ||
|
||
But if you prefer not to load the extra binary, you can compile the the code | ||
(only one file) into your own project. | ||
|
||
## Specification | ||
|
||
#### Methods | ||
##### getScopedCid(scope, apiKey, callback) | ||
- `scope`: The scope of the client ID. You can get different client IDs for the same user client by applying different `scope`. | ||
- `apiKey`: The API key to be used for the request. You can apply for your own API key [here](). TODO: add link to the console. | ||
- `callback`: The callback is a function taking 2 params: `function(err, cid)`. | ||
- `err`: an `Error` object if there is any error, otherwise `null`. | ||
- `cid`: the client ID string returned from the server. | ||
|
||
#### Client ID values | ||
The value of the returned client ID can be one of the following: | ||
- A string starting with `amp-` followed by URL safe Base64 characters. e.g: | ||
``` | ||
amp-UaFdEOQkTib3XGbPVGAJt0OQV8_1Hpmp8EsQOM5EySjmiK9UCs7yTCt219Fz2gER | ||
``` | ||
- `null`. The client ID was not found. | ||
- `undefined`. An error occured, check error message. | ||
- `'$OPT_OUT'`. The client has opted out client ID tracking. | ||
|
||
#### Cookie usage | ||
The library uses cookie `AMP_TOKEN` to store information. It serves 2 purposes: | ||
1. To persist a security token received from the API server which is to be used for exchanging CID next time. | ||
2. To act as a lock so that no concurrent requests being sent. | ||
|
||
|
||
## Run test | ||
``` | ||
$ npm install | ||
$ npm run test | ||
``` | ||
|
||
## Disclaimer | ||
This is not an official Google product. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<!-- | ||
Copyright 2017 Google Inc. | ||
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 | ||
https://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. | ||
--> | ||
|
||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<title>AMP Client ID library example</title> | ||
<script async src="index.js"></script> | ||
</head> | ||
<body> | ||
AMP Client ID library example | ||
<script> | ||
function ampCidApiOnload(callback) { | ||
(self.googleAmpCidApiOnload = self.googleAmpCidApiOnload || []).push(callback); | ||
} | ||
|
||
ampCidApiOnload(function(api) { | ||
api.getScopedCid('scope-abc', 'YOUR_APIKEY_HERE', function(err, cid) { | ||
if (err) { | ||
alert(err); | ||
} else { | ||
alert('Client ID:' + cid); | ||
} | ||
}); | ||
}); | ||
</script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
/** | ||
* Copyright 2017 Google Inc. | ||
* | ||
* 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 | ||
* | ||
* https://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. | ||
*/ | ||
|
||
var GOOGLE_API_URL = 'https://ampcid.google.com/v1/publisher:getClientId?key='; | ||
var AMP_TOKEN = 'AMP_TOKEN'; | ||
|
||
var TokenStatus = { | ||
RETRIEVING: '$RETRIEVING', | ||
OPT_OUT: '$OPT_OUT', | ||
NOT_FOUND: '$NOT_FOUND', | ||
ERROR: '$ERROR' | ||
}; | ||
|
||
var TIMEOUT = 30000; | ||
var HOUR = 60 * 60 * 1000; | ||
var DAY = 24 * HOUR; | ||
var YEAR = 365 * DAY; | ||
|
||
var PROXY_ORIGIN_URL_REGEX = | ||
/^https:\/\/([a-zA-Z0-9_-]+\.)?cdn\.ampproject\.org/; | ||
|
||
var scopedCidCallbacks = {}; | ||
|
||
function getScopedCid(scope, apiKey, callback) { | ||
if (scopedCidCallbacks[scope]) { | ||
scopedCidCallbacks[scope].push(callback); | ||
return; | ||
} | ||
scopedCidCallbacks[scope] = [callback]; | ||
var cb = wrapCallbacks(scopedCidCallbacks[scope]); | ||
var token; | ||
// Block the request if a previous request is on flight | ||
// Poll every 200ms. Longer interval means longer latency for the 2nd CID. | ||
poll(200, function () { | ||
token = getCookie(AMP_TOKEN); | ||
return token !== TokenStatus.RETRIEVING; | ||
}, function () { | ||
// If the page referrer is proxy origin, we force to use API even the | ||
// token indicates a previous fetch returned nothing | ||
var forceFetch = | ||
token === TokenStatus.NOT_FOUND && isReferrerProxyOrigin(); | ||
|
||
// Token is in a special state | ||
if (!forceFetch && isStatusToken(token)) { | ||
if (token === TokenStatus.OPT_OUT) { | ||
cb(null, TokenStatus.OPT_OUT); | ||
} else if (token === TokenStatus.NOT_FOUND) { | ||
cb(null, null); | ||
} else if (token === TokenStatus.ERROR) { | ||
cb(new Error('There was an error in previous request. Try later.')); | ||
} else { | ||
cb(new Error('Invalid token state: ' + token)); | ||
} | ||
return; | ||
} | ||
|
||
if (!token || isStatusToken(token)) { | ||
persistToken(TokenStatus.RETRIEVING, TIMEOUT); | ||
} | ||
fetchCid(GOOGLE_API_URL + apiKey, scope, token, cb) | ||
}); | ||
} | ||
|
||
function fetchCid(url, scope, token, callback) { | ||
var payload = {'originScope': scope}; | ||
if (token) { | ||
payload['securityToken'] = token; | ||
} | ||
fetchJson(url, payload, TIMEOUT, function (err, res) { | ||
if (err) { | ||
persistToken(TokenStatus.ERROR, TIMEOUT); | ||
callback(err); | ||
} else if (res['optOut']) { | ||
persistToken(TokenStatus.OPT_OUT, YEAR); | ||
callback(null, TokenStatus.OPT_OUT); | ||
} else if (res['clientId']) { | ||
persistToken(res['securityToken'], YEAR); | ||
callback(null, res['clientId']); | ||
} else { | ||
persistToken(TokenStatus.NOT_FOUND, HOUR); | ||
callback(null, null); | ||
} | ||
}); | ||
} | ||
|
||
function wrapCallbacks(callbacks) { | ||
return function () { | ||
var args = arguments; | ||
callbacks.forEach(function (callback) { | ||
callback.apply(null, args); | ||
}) | ||
}; | ||
} | ||
|
||
function persistToken(tokenValue, expires) { | ||
if (tokenValue) { | ||
setCookie(AMP_TOKEN, tokenValue, Date.now() + expires); | ||
} | ||
} | ||
|
||
function isReferrerProxyOrigin() { | ||
return PROXY_ORIGIN_URL_REGEX.test(self.document.referrer); | ||
} | ||
|
||
function isStatusToken(token) { | ||
return token && token[0] === '$'; | ||
} | ||
|
||
function poll(delay, predicate, callback) { | ||
var interval = self.setInterval(function () { | ||
if (predicate()) { | ||
self.clearInterval(interval); | ||
callback(); | ||
} | ||
}, delay); | ||
} | ||
|
||
function getCookie(name) { | ||
var cookies = self.document.cookie.split(';'); | ||
for (var i = 0; i < cookies.length; i++) { | ||
var cookie = cookies[i].trim(); | ||
var values = cookie.split('=', 2); | ||
if (values.length !== 2) { | ||
continue; | ||
} | ||
if (decodeURIComponent(values[0]) === name) { | ||
return decodeURIComponent(values[1]); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
function setCookie(name, value, expirationTime) { | ||
self.document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value) + | ||
'; path=/; expires=' + new Date(expirationTime).toUTCString(); | ||
} | ||
|
||
function fetchJson(url, payload, timeout, callback) { | ||
var oneTimeCallback = oneTime(callback); | ||
self.setTimeout(function () { | ||
oneTimeCallback(new Error('Request timed out')); | ||
}, timeout); | ||
|
||
var xhr = createPostXhrRequest(url); | ||
xhr.onreadystatechange = function () { | ||
if (xhr.readyState < /* STATUS_RECEIVED */ 2) { | ||
return; | ||
} | ||
if (xhr.status < 100 || xhr.status > 599) { | ||
xhr.onreadystatechange = null; | ||
oneTimeCallback(new Error('Unknown HTTP status' + xhr.status)); | ||
return; | ||
} | ||
if (xhr.readyState === /* COMPLETE */ 4) { | ||
try { | ||
var json = JSON.parse(xhr.responseText); | ||
if (xhr.status >= 400) { | ||
oneTimeCallback(new Error(json.error ? | ||
json.error.message : ('Invalid response: ' + xhr.responseText))); | ||
} else { | ||
oneTimeCallback(null, json); | ||
} | ||
} catch (e) { | ||
oneTimeCallback(new Error('Invalid response: ' + xhr.responseText)); | ||
} | ||
} | ||
}; | ||
xhr.onerror = function () { | ||
oneTimeCallback(new Error('Network failure')); | ||
}; | ||
xhr.onabort = function () { | ||
oneTimeCallback(new Error('Request aborted')); | ||
}; | ||
xhr.send(JSON.stringify(payload)); | ||
} | ||
|
||
function createPostXhrRequest(url) { | ||
var xhr = new self.XMLHttpRequest(); | ||
if ('withCredentials' in xhr) { | ||
xhr.open('POST', url, true); | ||
} else if (typeof self.XDomainRequest !== 'undefined') { | ||
// IE-specific object. | ||
xhr = new self.XDomainRequest(); | ||
xhr.open('POST', url); | ||
} else { | ||
throw new Error('CORS is not supported'); | ||
} | ||
xhr.withCredentials = true; | ||
xhr.setRequestHeader('Content-Type', 'text/plain;charset=utf-8'); | ||
xhr.setRequestHeader('Accept', 'application/json'); | ||
return xhr; | ||
} | ||
|
||
function oneTime(callback) { | ||
var called = false; | ||
return function () { | ||
if (!called) { | ||
callback.apply(null, arguments); | ||
called = true; | ||
} | ||
} | ||
} | ||
|
||
|
||
self.GoogleAmpCidApi = { | ||
getScopedCid: getScopedCid | ||
}; | ||
|
||
if (Object.prototype.toString.call(self.googleAmpCidApiOnload) === '[object Array]') { | ||
self.googleAmpCidApiOnload.forEach(function (listener) { | ||
listener(self.GoogleAmpCidApi); | ||
}); | ||
} | ||
|
||
self.googleAmpCidApiOnload = { | ||
push: function (listener) { | ||
listener(self.GoogleAmpCidApi); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
{ | ||
"name": "ampcidapi", | ||
"version": "1.0.0", | ||
"description": "AMP Client ID Library", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo 'var self = {};' > test.tmp.js; cat index.js test.js >> test.tmp.js; node_modules/mocha/bin/mocha test.tmp.js", | ||
"server": "./node_modules/http-server/bin/http-server" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/google/amp-client-id-library.git" | ||
}, | ||
"keywords": [ | ||
"AMP" | ||
], | ||
"author": "AMP team", | ||
"license": "Apache-2.0", | ||
"bugs": { | ||
"url": "https://github.com/google/amp-client-id-library/issues" | ||
}, | ||
"homepage": "https://github.com/google/amp-client-id-library#readme", | ||
"devDependencies": { | ||
"chai": "3.5.0", | ||
"http-server": "0.10.0", | ||
"mocha": "2.5.3", | ||
"sinon": "1.17.7", | ||
"sinon-chai": "2.8.0" | ||
} | ||
} |
Oops, something went wrong.