ASRouter Admin
Need help using these tools? Check out our{" "} documentation
{this.renderSection()}/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ import { ASRouterUtils } from "../../asrouter-utils.mjs"; import React from "react"; import ReactDOM from "react-dom"; import { SimpleHashRouter } from "./SimpleHashRouter"; import { CopyButton } from "./CopyButton"; import { ImpressionsSection } from "./ImpressionsSection"; // Convert a UTF-8 string to a string in which only one byte of each // 16-bit unit is occupied. This is necessary to comply with `btoa` API constraints. export function toBinary(string) { const codeUnits = new Uint16Array(string.length); for (let i = 0; i < codeUnits.length; i++) { codeUnits[i] = string.charCodeAt(i); } return btoa( String.fromCharCode(...Array.from(new Uint8Array(codeUnits.buffer))) ); } function relativeTime(timestamp) { if (!timestamp) { return ""; } const seconds = Math.floor((Date.now() - timestamp) / 1000); const minutes = Math.floor((Date.now() - timestamp) / 60000); if (seconds < 2) { return "just now"; } else if (seconds < 60) { return `${seconds} seconds ago`; } else if (minutes === 1) { return "1 minute ago"; } else if (minutes < 600) { return `${minutes} minutes ago`; } return new Date(timestamp).toLocaleString(); } export class ToggleMessageJSON extends React.PureComponent { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { this.props.toggleJSON(this.props.msgId); } render() { let direction = this.props.isCollapsed ? "forward" : "down"; return ( ); } } export class ASRouterAdminInner extends React.PureComponent { constructor(props) { super(props); this.handleEnabledToggle = this.handleEnabledToggle.bind(this); this.handleUserPrefToggle = this.handleUserPrefToggle.bind(this); this.onChangeFilters = this.onChangeFilters.bind(this); this.onClearFilters = this.onClearFilters.bind(this); this.unblockAll = this.unblockAll.bind(this); this.resetAllJSON = this.resetAllJSON.bind(this); this.handleExpressionEval = this.handleExpressionEval.bind(this); this.onChangeTargetingParameters = this.onChangeTargetingParameters.bind(this); this.onChangeAttributionParameters = this.onChangeAttributionParameters.bind(this); this.setAttribution = this.setAttribution.bind(this); this.onCopyTargetingParams = this.onCopyTargetingParams.bind(this); this.onNewTargetingParams = this.onNewTargetingParams.bind(this); this.resetMessageState = this.resetMessageState.bind(this); this.toggleJSON = this.toggleJSON.bind(this); this.toggleAllMessages = this.toggleAllMessages.bind(this); this.resetGroupImpressions = this.resetGroupImpressions.bind(this); this.onMessageFromParent = this.onMessageFromParent.bind(this); this.setStateFromParent = this.setStateFromParent.bind(this); this.setState = this.setState.bind(this); this.state = { filterGroups: [], filterProviders: [], filterTemplates: [], filtersCollapsed: true, collapsedMessages: [], modifiedMessages: [], messageBlockList: [], multiProfileMessageBlocklist: [], evaluationStatus: {}, stringTargetingParameters: null, newStringTargetingParameters: null, copiedToClipboard: false, attributionParameters: { source: "addons.mozilla.org", medium: "referral", campaign: "non-fx-button", content: `rta:${btoa("uBlock0@raymondhill.net")}`, experiment: "ua-onboarding", variation: "chrome", ua: "Google Chrome 123", dltoken: "00000000-0000-0000-0000-000000000000", }, }; } onMessageFromParent({ type, data }) { // These only exists due to onPrefChange events in ASRouter switch (type) { case "UpdateAdminState": { this.setStateFromParent(data); break; } } } async setStateFromParent(data) { await this.setState(data); if (!this.state.stringTargetingParameters) { const stringTargetingParameters = {}; for (const param of Object.keys(data.targetingParameters)) { stringTargetingParameters[param] = JSON.stringify( data.targetingParameters[param], null, 2 ); } await this.setState({ stringTargetingParameters }); } } componentWillMount() { ASRouterUtils.addListener(this.onMessageFromParent); const endpoint = ASRouterUtils.getPreviewEndpoint(); ASRouterUtils.sendMessage({ type: "ADMIN_CONNECT_STATE", data: { endpoint }, }).then(this.setStateFromParent); } componentWillUnmount() { ASRouterUtils.removeListener(this.onMessageFromParent); } handleBlock(msg) { ASRouterUtils.blockById(msg.id); } handleUnblock(msg) { ASRouterUtils.unblockById(msg.id); } resetJSON(msg) { // reset the displayed JSON for the given message let textarea = document.getElementById(`${msg.id}-textarea`); textarea.value = JSON.stringify(msg, null, 2); textarea.classList.remove("errorState"); // remove the message from the list of modified IDs let index = this.state.modifiedMessages.indexOf(msg.id); this.setState(prevState => ({ modifiedMessages: [ ...prevState.modifiedMessages.slice(0, index), ...prevState.modifiedMessages.slice(index + 1), ], })); } resetAllJSON() { // reset the displayed JSON for each modified message for (const msgId of this.state.modifiedMessages) { const msg = this.state.messages.find(m => m.id === msgId); const textarea = document.getElementById(`${msgId}-textarea`); if (textarea) { textarea.value = JSON.stringify(msg, null, 2); textarea.classList.remove("errorState"); } } this.setState({ modifiedMessages: [] }); } showMessage(msg) { if (msg.template === "pb_newtab") { ASRouterUtils.openPBWindow(msg.content); } else { ASRouterUtils.overrideMessage(msg.id).then(state => this.setStateFromParent(state) ); } } async resetMessageState() { await Promise.all([ ASRouterUtils.resetMessageImpressions(), ASRouterUtils.resetGroupImpressions(), ASRouterUtils.resetScreenImpressions(), ASRouterUtils.unblockAll(), ]); let data = await ASRouterUtils.sendMessage({ type: "ADMIN_CONNECT_STATE", data: { endpoint: ASRouterUtils.getPreviewEndpoint() }, }); await this.setStateFromParent(data); } expireCache() { ASRouterUtils.sendMessage({ type: "EXPIRE_QUERY_CACHE" }); } resetPref() { ASRouterUtils.sendMessage({ type: "RESET_PROVIDER_PREF" }); } resetGroupImpressions() { ASRouterUtils.resetGroupImpressions().then(this.setStateFromParent); } resetMessageImpressions() { ASRouterUtils.resetMessageImpressions().then(this.setStateFromParent); } handleExpressionEval() { const context = {}; for (const param of Object.keys(this.state.stringTargetingParameters)) { const value = this.state.stringTargetingParameters[param]; context[param] = value ? JSON.parse(value) : null; } ASRouterUtils.sendMessage({ type: "EVALUATE_JEXL_EXPRESSION", data: { expression: this.refs.expressionInput.value || "undefined", context, }, }).then(this.setStateFromParent); } onChangeTargetingParameters(event) { const { name: eventName } = event.target; const { value } = event.target; let targetingParametersError = null; try { JSON.parse(value); event.target.classList.remove("errorState"); } catch (e) { console.error(`Error parsing value of parameter ${eventName}`); event.target.classList.add("errorState"); targetingParametersError = { id: eventName }; } this.setState(({ stringTargetingParameters }) => { const updatedParameters = { ...stringTargetingParameters }; updatedParameters[eventName] = value; return { copiedToClipboard: false, evaluationStatus: {}, stringTargetingParameters: updatedParameters, targetingParametersError, }; }); } unblockAll() { return ASRouterUtils.unblockAll().then(this.setStateFromParent); } async handleEnabledToggle(event) { const provider = this.state.providerPrefs.find( p => p.id === event.target.dataset.provider ); const userPrefInfo = this.state.userPrefs; const isUserEnabled = provider.id in userPrefInfo ? userPrefInfo[provider.id] : true; const isSystemEnabled = provider.enabled; const isEnabling = event.target.checked; if (isEnabling) { if (!isUserEnabled) { await ASRouterUtils.sendMessage({ type: "SET_PROVIDER_USER_PREF", data: { id: provider.id, value: true }, }); } if (!isSystemEnabled) { await ASRouterUtils.sendMessage({ type: "ENABLE_PROVIDER", data: provider.id, }); } } else { await ASRouterUtils.sendMessage({ type: "DISABLE_PROVIDER", data: provider.id, }); } this.setState({ filterProviders: [] }); } handleUserPrefToggle(event) { const action = { type: "SET_PROVIDER_USER_PREF", data: { id: event.target.dataset.provider, value: event.target.checked }, }; ASRouterUtils.sendMessage(action); this.setState({ filterProviders: [] }); } onChangeFilters(event) { // this function handles both provider filter and group filter. the checkbox // will have dataset.provider if it's a provider checkbox, and dataset.group // if it's a group checkbox. let stateKey; let itemValue; let { checked } = event.target; if (event.target.dataset.provider) { stateKey = "filterProviders"; itemValue = event.target.dataset.provider; } else if (event.target.dataset.group) { stateKey = "filterGroups"; itemValue = event.target.dataset.group; } else if (event.target.dataset.template) { stateKey = "filterTemplates"; itemValue = event.target.dataset.template; } else { return; } this.setState(prevState => { let newValue; if (checked) { newValue = prevState[stateKey].includes(itemValue) ? prevState[stateKey] : prevState[stateKey].concat(itemValue); } else { newValue = prevState[stateKey].filter(item => item !== itemValue); } return { [stateKey]: newValue }; }); } onClearFilters() { this.setState({ filterProviders: [], filterGroups: [], filterTemplates: [], }); } // Simulate a copy event that sets to clipboard all targeting paramters and values onCopyTargetingParams() { const stringTargetingParameters = { ...this.state.stringTargetingParameters, }; for (const key of Object.keys(stringTargetingParameters)) { // If the value is not set the parameter will be lost when we stringify if (stringTargetingParameters[key] === undefined) { stringTargetingParameters[key] = null; } } const setClipboardData = e => { e.preventDefault(); e.clipboardData.setData( "text", JSON.stringify(stringTargetingParameters, null, 2) ); document.removeEventListener("copy", setClipboardData); this.setState({ copiedToClipboard: true }); }; document.addEventListener("copy", setClipboardData); document.execCommand("copy"); } onNewTargetingParams(event) { this.setState({ newStringTargetingParameters: event.target.value }); event.target.classList.remove("errorState"); this.refs.targetingParamsEval.innerText = ""; try { const stringTargetingParameters = JSON.parse(event.target.value); this.setState({ stringTargetingParameters }); } catch (e) { event.target.classList.add("errorState"); this.refs.targetingParamsEval.innerText = e.message; } } toggleJSON(msgId) { if (this.state.collapsedMessages.includes(msgId)) { let index = this.state.collapsedMessages.indexOf(msgId); this.setState(prevState => ({ collapsedMessages: [ ...prevState.collapsedMessages.slice(0, index), ...prevState.collapsedMessages.slice(index + 1), ], })); } else { this.setState(prevState => ({ collapsedMessages: prevState.collapsedMessages.concat(msgId), })); } } onMessageChanged(msgId) { if (!this.state.modifiedMessages.includes(msgId)) { this.setState(prevState => ({ modifiedMessages: prevState.modifiedMessages.concat(msgId), })); } } renderMessageItem(msg) { const isBlockedByGroup = this.state.groups .filter(group => msg.groups.includes(group.id)) .some(group => !group.enabled); const msgProvider = this.state.providers.find(provider => provider.id === msg.provider) || {}; const isProviderExcluded = msgProvider.exclude && msgProvider.exclude.includes(msg.id); const isMessageBlocked = this.state.messageBlockList.includes(msg.id) || this.state.messageBlockList.includes(msg.campaign) || this.state.multiProfileMessageBlocklist.includes(msg.id); const isBlocked = isMessageBlocked || isBlockedByGroup || isProviderExcluded; const impressions = this.state.messageImpressions[msg.id] ? this.state.messageImpressions[msg.id].length : 0; const isCollapsed = this.state.collapsedMessages.includes(msg.id); const isModified = this.state.modifiedMessages.includes(msg.id); const aboutMessagePreviewSupported = [ "infobar", "spotlight", "cfr_doorhanger", "feature_callout", "pb_newtab", ].includes(msg.template); let itemClassName = "message-item"; if (isBlocked) { itemClassName += " blocked"; } let messageStats = []; let messageStatsString; if (impressions) { messageStats.push(`${impressions} impressions`); } if (isMessageBlocked) { messageStats.push("message blocked"); } else if (isBlockedByGroup) { messageStats.push("message group blocked"); } else if (isProviderExcluded) { messageStats.push("excluded by provider"); } if (messageStats.length) { messageStatsString = `(${messageStats.join(", ")})`; } return (
| Provider | Source | Last Updated | |
| {isTestProvider ? ( ) : ( )} | {provider.id} | {label} | {info.lastUpdated ? relativeTime(info.lastUpdated) : ""} |
| Group | Impressions | Frequency caps | User preferences | |
| {id} | {this._getGroupImpressionsCount(id, frequency)} | {frequencyCaps.join(", ")} | {userPreferences.join(", ")} |
Evaluate JEXL expression |
|
|
Status: {success ? "✅" : "❌"}
|
|
Modify targeting parameters |
|
| {param} | {inputComp} |
Attribution parameters |
|
|
This forces the browser to set some attribution parameters, useful for testing the Return To AMO feature. Clicking on 'Force Attribution', with the default values in each field, will demo the Return To AMO flow with the addon called 'uBlock Origin'. If you wish to try different attribution parameters, enter them in the text boxes. If you wish to try a different addon with the Return To AMO flow, make sure the 'content' text box has a string that is 'rta:base64(addonID)', the base64 string of the addonID prefixed with 'rta:'. The addon must currently be a recommended addon on AMO. Then click 'Force Attribution'. Clicking on 'Force Attribution' with blank text boxes reset attribution data. |
|
| Source | |
| Medium | |
| Campaign | |
| Content | |
| Experiment | |
| Variation | |
| User Agent | |
| Download Token | |
| Provider | Message | Timestamp |
|---|
No errors
; } renderSection() { const [section] = this.props.location.routes; switch (section) { case "targeting": return (
browser.newtabpage.activity-stream.asrouter.devtoolsEnabled
{" "}
to true and then reloading this page.
Need help using these tools? Check out our{" "} documentation
{this.renderSection()}