/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
2603010454000
src/impression.js
340 строк
10 KB
Ryan Cebulko
♻️ Move files into #core, #utils to enable tsc typechecking (#36277)
07 окт 2021, 23:26
Не верифицирован
07 окт 2021, 23:26
b6d1550
Код
Авторство
О чём код?
import {Deferred} from '#core/data-structures/promise'; import {parseQueryString} from '#core/types/string/url'; import {WindowInterface} from '#core/window/interface'; import {isExperimentOn} from '#experiments'; import {Services} from '#service'; import {dev, user, userAssert} from '#utils/log'; import {getMode} from './mode'; import {addParamsToUrl, isProxyOrigin, parseUrlDeprecated} from './url'; const TIMEOUT_VALUE = 8000; let trackImpressionPromise = null; const DEFAULT_APPEND_URL_PARAM = ['gclid', 'gclsrc']; /** * These domains are trusted with more sensitive viewer operations such as * sending impression requests. If you believe your domain should be here, * file the issue on GitHub to discuss. The process will be similar * (but somewhat more stringent) to the one described in the [3p/README.md]( * https://github.com/ampproject/amphtml/blob/main/3p/README.md) * * @type {!Array<!RegExp>} */ const TRUSTED_REFERRER_HOSTS = [ /** * Twitter's link wrapper domains: * - t.co */ /^t.co$/, ]; /** * A function to get the trackImpressionPromise; * @return {!Promise} */ export function getTrackImpressionPromise() { return userAssert(trackImpressionPromise, 'E#19457 trackImpressionPromise'); } /** * Function that reset the trackImpressionPromise only for testing * @visibleForTesting */ export function resetTrackImpressionPromiseForTesting() { trackImpressionPromise = null; } /** * Emit a HTTP request to a destination defined on the incoming URL. * Launched for trusted viewer. Otherwise guarded by experiment. * @param {!Window} win */ export function maybeTrackImpression(win) { const deferred = new Deferred(); const {promise, resolve: resolveImpression} = deferred; trackImpressionPromise = Services.timerFor(win) .timeoutPromise(TIMEOUT_VALUE, promise, 'TrackImpressionPromise timeout') .catch((error) => { dev().warn('IMPRESSION', error); }); const viewer = Services.viewerForDoc(win.document.documentElement); const isTrustedViewerPromise = viewer.isTrustedViewer(); const isTrustedReferrerPromise = viewer .getReferrerUrl() .then((referrer) => isTrustedReferrer(referrer)); Promise.all([isTrustedViewerPromise, isTrustedReferrerPromise]).then( (results) => { const isTrustedViewer = results[0]; const isTrustedReferrer = results[1]; // Enable the feature in the case of trusted viewer, // or trusted referrer // or with experiment turned on if ( !isTrustedViewer && !isTrustedReferrer && !isExperimentOn(win, 'alp') ) { resolveImpression(); return; } const replaceUrlPromise = handleReplaceUrl(win); const clickUrlPromise = handleClickUrl(win); Promise.all([replaceUrlPromise, clickUrlPromise]).then( () => { resolveImpression(); }, () => {} ); } ); } /** * Signal that impression tracking is not relevant in this environment. */ export function doNotTrackImpression() { trackImpressionPromise = Promise.resolve(); } /** * Handle the getReplaceUrl and return a promise when url is replaced Only * handles replaceUrl when viewer indicates AMP to do so. Viewer should indicate * by setting the legacy replaceUrl init param and add `replaceUrl` to its * capability param. Future plan is to change the type of legacy init replaceUrl * param from url string to boolean value. Please NOTE replaceUrl and adLocation * will never arrive at same time, so there is no race condition on the order of * handling url replacement. * @param {!Window} win * @return {!Promise} */ function handleReplaceUrl(win) { const viewer = Services.viewerForDoc(win.document.documentElement); // ReplaceUrl substitution doesn't have to wait until the document is visible if (!viewer.getParam('replaceUrl')) { // The init replaceUrl param serve as a signal on whether replaceUrl is // required for this doc. return Promise.resolve(); } if (!viewer.hasCapability('replaceUrl')) { // If Viewer is not capability of providing async replaceUrl, use the legacy // init replaceUrl param. viewer.replaceUrl(viewer.getParam('replaceUrl') || null); return Promise.resolve(); } // request async replaceUrl is viewer support getReplaceUrl. return viewer .sendMessageAwaitResponse('getReplaceUrl', /* data */ undefined) .then( (response) => { if (!response || typeof response != 'object') { dev().warn('IMPRESSION', 'get invalid replaceUrl response'); return; } viewer.replaceUrl(response['replaceUrl'] || null); }, (err) => { dev().warn('IMPRESSION', 'Error request replaceUrl from viewer', err); } ); } /** * @param {string} referrer * @return {boolean} * @visibleForTesting */ export function isTrustedReferrer(referrer) { const url = parseUrlDeprecated(referrer); if (url.protocol != 'https:') { return false; } return TRUSTED_REFERRER_HOSTS.some((th) => th.test(url.hostname)); } /** * Perform the impression request if it has been provided via * the click param in the viewer arguments. Returns a promise. * @param {!Window} win * @return {!Promise} */ function handleClickUrl(win) { const ampdoc = Services.ampdoc(win.document.documentElement); const viewer = Services.viewerForDoc(ampdoc); /** @const {?string} */ const clickUrl = viewer.getParam('click'); if (!clickUrl) { return Promise.resolve(); } if (clickUrl.indexOf('https://') != 0) { user().warn( 'IMPRESSION', 'click fragment param should start with https://. Found ', clickUrl ); return Promise.resolve(); } if (WindowInterface.getLocation(win).hash) { // This is typically done using replaceState inside the viewer. // If for some reason it failed, get rid of the fragment here to // avoid duplicate tracking. WindowInterface.getLocation(win).hash = ''; } // TODO(@zhouyx) need test with a real response. return ampdoc .whenFirstVisible() .then(() => { return invoke(win, dev().assertString(clickUrl)); }) .then((response) => { applyResponse(win, response); }) .catch((err) => { user().warn('IMPRESSION', 'Error on request clickUrl: ', err); }); } /** * Send the url to ad server and wait for its response * @param {!Window} win * @param {string} clickUrl * @return {!Promise<?JsonObject>} */ function invoke(win, clickUrl) { if (getMode().localDev && !getMode().test) { clickUrl = 'http://localhost:8000/impression-proxy?url=' + clickUrl; } return Services.xhrFor(win) .fetchJson(clickUrl, { credentials: 'include', }) .then((res) => { // Treat 204 no content response specially if (res.status == 204) { return null; } return res.json(); }); } /** * parse the response back from ad server * Set for analytics purposes * @param {!Window} win * @param {?JsonObject} response */ function applyResponse(win, response) { if (!response) { return; } const adLocation = response['location']; const adTracking = response['tracking_url']; // If there is a tracking_url, need to track it // Otherwise track the location const trackUrl = adTracking || adLocation; if (trackUrl && !isProxyOrigin(trackUrl)) { // To request the provided trackUrl for tracking purposes. new Image().src = trackUrl; } // Replace the location href params with new location params we get (if any). if (adLocation) { if (!win.history.replaceState) { return; } const viewer = Services.viewerForDoc(win.document.documentElement); const currentHref = WindowInterface.getLocation(win).href; const url = parseUrlDeprecated(adLocation); const params = parseQueryString(url.search); const newHref = addParamsToUrl(currentHref, params); // TODO: Avoid overwriting the fragment parameter. win.history.replaceState(null, '', newHref); viewer.maybeUpdateFragmentForCct(); } } /** * Return a promise that whether appending extra url params to outgoing link is * required. * @param {!./service/ampdoc-impl.AmpDoc} ampdoc * @return {!Promise<boolean>} */ export function shouldAppendExtraParams(ampdoc) { return ampdoc.whenReady().then(() => { return !!ampdoc .getBody() .querySelector('amp-analytics[type=googleanalytics]'); }); } /** * Return the extra url params string that should be appended to outgoing link * @param {!Window} win * @param {!Element} target * @return {string} */ export function getExtraParamsUrl(win, target) { // Get an array with extra params that needs to append. const url = parseUrlDeprecated(WindowInterface.getLocation(win).href); const params = parseQueryString(url.search); const appendParams = []; for (let i = 0; i < DEFAULT_APPEND_URL_PARAM.length; i++) { const param = DEFAULT_APPEND_URL_PARAM[i]; if (typeof params[param] !== 'undefined') { appendParams.push(param); } } // Check if the param already exists const additionalUrlParams = target.getAttribute('data-amp-addparams'); let {href} = target; if (additionalUrlParams) { href = addParamsToUrl(href, parseQueryString(additionalUrlParams)); } const loc = parseUrlDeprecated(href); const existParams = parseQueryString(loc.search); for (let i = appendParams.length - 1; i >= 0; i--) { const param = appendParams[i]; if (typeof existParams[param] !== 'undefined') { appendParams.splice(i, 1); } } return getQueryParamUrl(appendParams); } /** * Helper method to convert an query param array to string * @param {!Array<string>} params * @return {string} */ function getQueryParamUrl(params) { let url = ''; for (let i = 0; i < params.length; i++) { const param = params[i]; url += i == 0 ? `${param}=QUERY_PARAM(${param})` : `&${param}=QUERY_PARAM(${param})`; } return url; }