├── .eslintrc.cjs ├── .gcloudignore ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── app.js ├── cloudbuild.yaml ├── index.js ├── package-lock.json ├── package.json ├── renovate.json ├── routes └── error-tracker.js ├── test ├── .eslintrc.cjs ├── _init-tests.js ├── e2e │ └── test-errortracker.js └── unit │ ├── test-cache.js │ ├── test-frame.js │ ├── test-human-rtv.js │ ├── test-log-target.js │ ├── test-query-string.js │ ├── test-should-ignore.js │ ├── test-standardize-stack-trace.js │ └── test-unminify.js └── utils ├── cache.js ├── log-target.js ├── log.js ├── requests ├── extract-reporting-params.js ├── parse-error-handling.js └── query-string.js ├── rtv ├── human-rtv.js ├── latest-rtv.js └── release-channels.js └── stacktrace ├── frame.js ├── should-ignore.js ├── standardize-stack-trace.js └── unminify.js /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['sort-destructure-keys'], 5 | env: { 6 | es6: true, 7 | node: true, 8 | }, 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:import/recommended', 12 | 'plugin:@typescript-eslint/recommended', 13 | 'plugin:prettier/recommended', 14 | ], 15 | parserOptions: { 16 | ecmaVersion: 'latest', 17 | sourceType: 'module', 18 | }, 19 | rules: { 20 | 'sort-destructure-keys/sort-destructure-keys': 'error', 21 | '@typescript-eslint/no-unused-vars': [ 22 | 'error', 23 | { varsIgnorePattern: 'unused' }, 24 | ], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | if: github.repository == 'ampproject/error-tracker' 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: lts/* 20 | cache: npm 21 | - run: npm ci 22 | - run: npm run lint 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | See https://github.com/ampproject/meta/blob/master/CODE_OF_CONDUCT.md 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017, Google Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Error reporting 2 | 3 | This is not an official Google product 4 | 5 | Receives error reports emitted by AMP HTML runtime library and sends them to the 6 | [Google Cloud Error Logging service](https://cloud.google.com/error-reporting/). 7 | 8 | This tool does not collect any user data or information. 9 | 10 | ## Setup 11 | 12 | 1. Enable Google Cloud Logging API. 13 | 2. Authenticate with Google Cloud: `$ gcloud auth login` 14 | 3. Start the server: `$ npm start` 15 | 16 | ## Deployments 17 | 18 | This application runs on [Google Cloud Functions](https://cloud.google.com/functions). There are three endpoints that execute the same functionality: 19 | 20 | - `/r` - 90% of traffic goes to this endpoint 21 | - `/r-beta` - 10% of traffic goes to this endpoint 22 | - `/r-dev` - only manual traffic goes to this endpoint 23 | 24 | Note that [amphtml](https://github.com/ampproject/amphtml), by default, sends reports to https://us-central1-amp-error-reporting.cloudfunctions.net/r and to https://us-central1-amp-error-reporting.cloudfunctions.net/r-beta. This is considered the canonical error reporting service. 25 | 26 | ### Deploying to `/r-dev` 27 | 28 | Any developer with a Google Cloud Project that was set up as above can deploy to the `/r-dev` endpoint of their project by running `npm run deploy-dev`. This action will directly deploy the function to GCP. 29 | 30 | ### Deploying to `/r-beta` 31 | 32 | Commits merged to this repository's `main` branch are automatically deployed to the `/r-beta` endpoint on the canonical error reporting service using a [Cloud Build](https://cloud.google.com/build) action, defined in the [cloudbuild.yaml](./cloudbuild.yaml) config file. 33 | 34 | ### Deploying to `/r` 35 | 36 | This action can only be performed by GitHub users with write permission on this repository. To deploy to the production/stable endpoint `/r`, run `npm run deploy-stable`. This will create and push a Git tag of the form `deploy-stable-YYMMDDHHMMSS`, which in turn triggers a Cloud Build action similar to the beta environment. 37 | 38 | ## License 39 | 40 | Licensed under the Apache 2.0 license 41 | http://www.apache.org/licenses/LICENSE-2.0 42 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import express from 'express'; 18 | import { StatusCodes } from 'http-status-codes'; 19 | 20 | import { errorTracker } from './routes/error-tracker.js'; 21 | import { parseErrorHandling } from './utils/requests/parse-error-handling.js'; 22 | 23 | const BODY_LIMIT = 10 * 1024; /* 10kb */ 24 | const jsonParser = express.json({ 25 | limit: BODY_LIMIT, 26 | type: () => true, 27 | }); 28 | 29 | const app = express(); 30 | function rawJsonBodyParserMiddleware(req, res, next) { 31 | if (!req.rawBody) { 32 | // Defer to bodyParser when running as a server. 33 | jsonParser(req, res, next); 34 | } else if (req.rawBody.length > BODY_LIMIT) { 35 | // When Cloud Functions hijacks the request, validate and parse it manually. 36 | next(StatusCodes.REQUEST_TOO_LONG); 37 | } else { 38 | req.body = JSON.parse(req.rawBody.toString()); 39 | next(); 40 | } 41 | } 42 | 43 | app.set('etag', false); 44 | app.set('trust proxy', true); 45 | 46 | // Parse the JSON request body 47 | app.use(rawJsonBodyParserMiddleware); 48 | // Handle BodyParser PayloadTooLargeError errors 49 | app.use(parseErrorHandling); 50 | 51 | app.post('*', (req, res) => { 52 | // Allow non-credentialed posts from anywhere. 53 | // Not strictly necessary, but it avoids an error being reported by the 54 | // browser. 55 | res.set('Access-Control-Allow-Origin', '*'); 56 | return errorTracker(req, res); 57 | }); 58 | 59 | export default app; 60 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' 3 | args: 4 | - gcloud 5 | - functions 6 | - deploy 7 | - ${_DEPLOY_VERSION} 8 | - --runtime=${_RUNTIME} 9 | - --set-env-vars 10 | - COMMIT_SHA=$COMMIT_SHA 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import app from './app.js'; 18 | 19 | export { app }; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "error-tracker", 4 | "version": "1.0.0", 5 | "description": "Receives error reports emitted by AMP HTML runtime library", 6 | "author": "The AMP HTML Authors", 7 | "license": "Apache-2.0", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/ampproject/error-tracker" 11 | }, 12 | "type": "module", 13 | "scripts": { 14 | "postinstall": "cd node_modules/safe-decode-uri-component && node-gyp rebuild", 15 | "dev": "functions-framework --target=app", 16 | "deploy-stable": "git tag 'deploy-stable-'`date -u '+%Y%m%d%H%M%S'` && git push git@github.com:ampproject/error-tracker --tags", 17 | "deploy-dev": "gcloud functions deploy r-dev --set-env-vars COMMIT_SHA=$(git rev-parse HEAD)", 18 | "lint": "eslint .", 19 | "test": "mocha test/*.js test/**/*.js" 20 | }, 21 | "dependencies": { 22 | "@google-cloud/logging": "11.2.0", 23 | "@google-cloud/storage": "7.13.0", 24 | "@jridgewell/trace-mapping": "0.3.25", 25 | "express": "4.21.0", 26 | "http-status-codes": "2.3.0", 27 | "lodash.debounce": "4.0.8", 28 | "node-fetch": "3.3.2", 29 | "safe-decode-uri-component": "1.2.2-native" 30 | }, 31 | "devDependencies": { 32 | "@google-cloud/functions-framework": "3.4.2", 33 | "@types/express": "4.17.21", 34 | "@types/lodash.debounce": "4.0.9", 35 | "@typescript-eslint/eslint-plugin": "7.18.0", 36 | "@typescript-eslint/parser": "7.18.0", 37 | "chai": "5.1.1", 38 | "eslint": "8.57.1", 39 | "eslint-config-prettier": "9.1.0", 40 | "eslint-plugin-chai-expect": "3.1.0", 41 | "eslint-plugin-import": "2.31.0", 42 | "eslint-plugin-sort-destructure-keys": "2.0.0", 43 | "eslint-plugin-prettier": "5.2.1", 44 | "mocha": "10.7.3", 45 | "nock": "13.5.5", 46 | "prettier": "3.3.3", 47 | "sinon": "18.0.1", 48 | "source-map": "0.7.4", 49 | "superagent": "9.0.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:recommended"], 3 | "commitMessagePrefix": "📦", 4 | "timezone": "America/Los_Angeles", 5 | "schedule": "after 12am every weekday", 6 | "dependencyDashboard": true, 7 | "packageRules": [ 8 | { 9 | "groupName": "devDependencies", 10 | "matchFileNames": ["package.json"], 11 | "matchDepTypes": ["devDependencies"], 12 | "automerge": true 13 | }, 14 | { 15 | "groupName": "dependencies", 16 | "matchFileNames": ["package.json"], 17 | "matchDepTypes": ["dependencies"], 18 | "automerge": false 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /routes/error-tracker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS-IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | /** 16 | * @fileoverview 17 | * Receive requests, handle edge cases, extract information and send it 18 | * to unmininification. 19 | */ 20 | 21 | import { StatusCodes } from 'http-status-codes'; 22 | import { extractReportingParams } from '../utils/requests/extract-reporting-params.js'; 23 | import { LoggingTarget } from '../utils/log-target.js'; 24 | import { standardizeStackTrace } from '../utils/stacktrace/standardize-stack-trace.js'; 25 | import { shouldIgnore } from '../utils/stacktrace/should-ignore.js'; 26 | import { unminify } from '../utils/stacktrace/unminify.js'; 27 | import { latestRtv } from '../utils/rtv/latest-rtv.js'; 28 | 29 | const CF_METADATA = { 30 | resource: { 31 | type: 'cloud_function', 32 | labels: { 33 | function_name: process.env.K_SERVICE, 34 | }, 35 | }, 36 | severity: 500, // Error. 37 | }; 38 | 39 | /** Logs an event to Stackdriver. */ 40 | async function logEvent(log, event) { 41 | await log.write(log.entry(CF_METADATA, event)); 42 | } 43 | 44 | /** 45 | * Construct an event object for logging. 46 | * @param {!Request} req 47 | * @param {!Object} reportingParams 48 | * @param {!LoggingTarget} logTarget 49 | * @return {Promise>} event object, or `null` if the 50 | * error should be ignored. 51 | */ 52 | async function buildEvent(req, reportingParams, logTarget) { 53 | const { buildQueryString, message, stacktrace, version } = reportingParams; 54 | 55 | const userAgent = req.get('User-Agent'); 56 | if (userAgent.includes('Googlebot')) { 57 | console.warn(`Ignored Googlebot errror report: ${message}`); 58 | return null; 59 | } 60 | 61 | const stack = standardizeStackTrace(stacktrace, message); 62 | if (shouldIgnore(message, stack)) { 63 | console.warn(`Ignored "${message}`); 64 | return null; 65 | } 66 | const unminifiedStack = await unminify(stack, version); 67 | 68 | const reqUrl = 69 | req.method === 'POST' 70 | ? `${req.originalUrl}?${buildQueryString()}` 71 | : req.originalUrl; 72 | const normalizedMessage = /^[A-Z][a-z]+: /.test(message) 73 | ? message 74 | : `Error: ${message}`; 75 | 76 | return { 77 | serviceContext: { 78 | service: logTarget.serviceName, 79 | version: logTarget.versionId, 80 | }, 81 | message: [normalizedMessage, ...unminifiedStack].join('\n'), 82 | context: { 83 | httpRequest: { 84 | method: req.method, 85 | url: reqUrl, 86 | userAgent, 87 | referrer: req.get('Referrer'), 88 | }, 89 | }, 90 | }; 91 | } 92 | /** 93 | * Extracts relevant information from request, handles edge cases and prepares 94 | * entry object to be logged and sends it to unminification. 95 | * @param {Request} req 96 | * @param {Response} res 97 | * @param {!Object} params 98 | * @return {?Promise} May return a promise that rejects on logging error 99 | */ 100 | export async function errorTracker(req, res) { 101 | const referrer = req.get('Referrer'); 102 | const params = req.body; 103 | const reportingParams = extractReportingParams(params); 104 | const { debug, message, version } = reportingParams; 105 | const logTarget = new LoggingTarget(referrer, reportingParams); 106 | const { log } = logTarget; 107 | 108 | // Reject requests missing essential info. 109 | if (!referrer || !version || !message) { 110 | return res.sendStatus(StatusCodes.BAD_REQUEST); 111 | } 112 | // Accept but ignore requests that get throttled. 113 | if ( 114 | version.includes('internalRuntimeVersion') || 115 | Math.random() > logTarget.throttleRate 116 | ) { 117 | return res.sendStatus(StatusCodes.OK); 118 | } 119 | 120 | const rtvs = await latestRtv(); 121 | // Drop requests from RTVs that are no longer being served. 122 | if (rtvs.length > 0 && !rtvs.includes(version)) { 123 | return res.sendStatus(StatusCodes.GONE); 124 | } 125 | 126 | let event; 127 | try { 128 | event = await buildEvent(req, reportingParams, logTarget); 129 | } catch (unminifyError) { 130 | console.warn('Error unminifying:', unminifyError); 131 | return res.sendStatus(StatusCodes.UNPROCESSABLE_ENTITY); 132 | } 133 | 134 | // Drop reports of errors that should be ignored. 135 | if (!event) { 136 | return res.sendStatus(StatusCodes.BAD_REQUEST); 137 | } 138 | 139 | const debugInfo = { 140 | event, 141 | metaData: CF_METADATA, 142 | projectId: log.logging.projectId, 143 | }; 144 | 145 | // Accept the error report and try to log it. 146 | res.status(StatusCodes.ACCEPTED); 147 | try { 148 | await logEvent(log, event); 149 | } catch (err) { 150 | console.warn('Error writing to log: ', err); 151 | debugInfo.error = err.stack; 152 | } finally { 153 | if (debug) { 154 | res.set('Content-Type', 'application/json; charset=utf-8'); 155 | res.send(debugInfo); 156 | } else { 157 | res.end(); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /test/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['../.eslintrc.cjs', 'plugin:chai-expect/recommended'], 3 | env: { 4 | mocha: true, 5 | }, 6 | globals: { 7 | chai: 'readonly', 8 | expect: 'readonly', 9 | sinon: 'readonly', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /test/_init-tests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | process.env.NODE_ENV = 'test'; 18 | 19 | import * as chai from 'chai'; 20 | import sinon from 'sinon'; 21 | 22 | global.chai = chai; 23 | global.expect = chai.expect; 24 | global.sinon = sinon; 25 | -------------------------------------------------------------------------------- /test/e2e/test-errortracker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions andc 14 | * limitations under the License. 15 | */ 16 | 17 | import { Log } from '@google-cloud/logging'; 18 | import { StatusCodes } from 'http-status-codes'; 19 | 20 | import app from '../../app.js'; 21 | import nock from 'nock'; 22 | import superagent from 'superagent'; 23 | 24 | describe('Error Tracker Server', () => { 25 | const makeQuery = (function () { 26 | const mappings = { 27 | version: 'v', 28 | message: 'm', 29 | stack: 's', 30 | runtime: 'rt', 31 | assert: 'a', 32 | canary: 'ca', 33 | expected: 'ex', 34 | debug: 'debug', 35 | thirdParty: '3p', 36 | binaryType: 'bt', 37 | prethrottled: 'pt', 38 | singlePassType: 'spt', 39 | }; 40 | const booleans = [ 41 | 'assert', 42 | 'canary', 43 | 'expected', 44 | 'debug', 45 | 'thirdParty', 46 | 'prethrottled', 47 | ]; 48 | 49 | return function makeQuery(options) { 50 | const query = {}; 51 | for (const prop in options) { 52 | let value; 53 | if (booleans.includes(prop)) { 54 | value = options[prop] ? '1' : '0'; 55 | } else { 56 | value = options[prop]; 57 | } 58 | query[mappings[prop]] = value; 59 | } 60 | 61 | return query; 62 | }; 63 | })(); 64 | 65 | function makePostRequest(type) { 66 | return function (referrer, query) { 67 | const q = makeQuery(query); 68 | return superagent 69 | .post(`http://127.0.0.1:${server.address().port}/r`) 70 | .ok(() => true) 71 | .type(type) 72 | .set('Referer', referrer) 73 | .set('User-Agent', userAgent) 74 | .send(type === 'json' ? q : JSON.stringify(q)); 75 | }; 76 | } 77 | 78 | const referrer = 'https://cdn.ampproject.org/'; 79 | const userAgent = 'Google Chrome blah blah version'; 80 | const knownGoodQuery = Object.freeze({ 81 | version: '011830043289240', 82 | // chai.request will encode this for us. 83 | message: 'The object does not support the operation or argument.', 84 | assert: false, 85 | runtime: '1p', 86 | binaryType: 'production', 87 | // chai.request will encode this for us. 88 | stack: 'Error: stuff\n at file.js:1:2\n at n (file2.js:3:4)', 89 | }); 90 | const rawSourceMap = { 91 | version: 3, 92 | file: 'min.js', 93 | names: ['bar', 'baz', 'n'], 94 | sources: ['one.js', 'two.js'], 95 | sourcesContent: [ 96 | ' ONE.foo = function (bar) {\n return baz(bar);\n };', 97 | ' TWO.inc = function (n) {\n return n + 1;\n };', 98 | ], 99 | sourceRoot: 'https://cdn.ampproject.org', 100 | mappings: 101 | 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;' + 102 | 'CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA', 103 | }; 104 | 105 | /** @type {sinon.SinonSandbox} */ 106 | let sandbox; 107 | /** @type {sinon.SinonFakeTimers} */ 108 | let clock; 109 | /** @type {import('node:http').Server} */ 110 | let server; 111 | 112 | before(() => { 113 | server = app.listen(0); 114 | }); 115 | after(() => { 116 | server.close(); 117 | }); 118 | 119 | beforeEach(async () => { 120 | nock.disableNetConnect(); 121 | nock.enableNetConnect('127.0.0.1'); 122 | 123 | sandbox = sinon.createSandbox({ 124 | useFakeTimers: true, 125 | }); 126 | clock = sandbox.clock; 127 | sandbox.stub(Log.prototype, 'write').resolves(); 128 | }); 129 | 130 | afterEach(async () => { 131 | await clock.tickAsync(1e10); 132 | 133 | sandbox.restore(); 134 | 135 | expect(nock.pendingMocks()).to.be.empty; 136 | nock.cleanAll(); 137 | }); 138 | 139 | testSuite('POST JSON', makePostRequest('json')); 140 | testSuite('POST Text', makePostRequest('text/plain')); 141 | 142 | function testSuite(description, makeRequest) { 143 | describe(description, () => { 144 | describe('rejects bad requests', () => { 145 | it('without referrer', async () => { 146 | const { text } = await makeRequest('', knownGoodQuery); 147 | expect(text).to.equal('Bad Request'); 148 | }); 149 | 150 | it('without version', async () => { 151 | const query = Object.assign({}, knownGoodQuery, { version: '' }); 152 | const { text } = await makeRequest(referrer, query); 153 | expect(text).to.equal('Bad Request'); 154 | }); 155 | 156 | it('without error message', async () => { 157 | const query = Object.assign({}, knownGoodQuery, { message: '' }); 158 | const { text } = await makeRequest(referrer, query); 159 | expect(text).to.equal('Bad Request'); 160 | }); 161 | 162 | it('with blacklisted error', async () => { 163 | const query = Object.assign({}, knownGoodQuery, { 164 | message: 'stop_youtube', 165 | }); 166 | 167 | sandbox.stub(Math, 'random').returns(0); 168 | const { text } = await makeRequest(referrer, query); 169 | expect(text).to.equal('Bad Request'); 170 | }); 171 | }); 172 | 173 | it('ignores development errors', async () => { 174 | const query = Object.assign({}, knownGoodQuery, { 175 | version: '$internalRuntimeVersion$', 176 | }); 177 | 178 | const { status } = await makeRequest(referrer, query); 179 | expect(status).to.equal(StatusCodes.OK); 180 | }); 181 | 182 | describe('throttling', () => { 183 | it('does not throttle canary dev errors', async () => { 184 | const query = Object.assign({}, knownGoodQuery, { canary: true }); 185 | 186 | sandbox.stub(Math, 'random').returns(1); 187 | const { status } = await makeRequest(referrer, query); 188 | expect(status).to.equal(StatusCodes.ACCEPTED); 189 | }); 190 | 191 | it('does not throttle "control" binary type errors', async () => { 192 | const query = Object.assign({}, knownGoodQuery, { 193 | binaryType: 'control', 194 | }); 195 | 196 | sandbox.stub(Math, 'random').returns(1); 197 | const { status } = await makeRequest(referrer, query); 198 | expect(status).to.equal(StatusCodes.ACCEPTED); 199 | }); 200 | 201 | it('throttles 90% of production errors', async () => { 202 | const query = Object.assign({}, knownGoodQuery); 203 | 204 | sandbox.stub(Math, 'random').returns(0.1); 205 | const response1 = await makeRequest(referrer, query); 206 | expect(response1.status).to.equal(StatusCodes.ACCEPTED); 207 | 208 | Math.random.returns(0.11); 209 | const response2 = await makeRequest(referrer, query); 210 | expect(response2.status).to.equal(StatusCodes.OK); 211 | }); 212 | 213 | it('does not throttles pre-throttled production errors', async () => { 214 | const query = Object.assign({ prethrottled: true }, knownGoodQuery); 215 | 216 | sandbox.stub(Math, 'random').returns(0.99); 217 | const { status } = await makeRequest(referrer, query); 218 | expect(status).to.equal(StatusCodes.ACCEPTED); 219 | }); 220 | 221 | it('throttles 90% of canary user errors', async () => { 222 | const query = Object.assign({}, knownGoodQuery, { 223 | canary: true, 224 | assert: true, 225 | }); 226 | 227 | sandbox.stub(Math, 'random').returns(0.1); 228 | const response1 = await makeRequest(referrer, query); 229 | expect(response1.status).to.equal(StatusCodes.ACCEPTED); 230 | 231 | Math.random.returns(0.11); 232 | const response2 = await makeRequest(referrer, query); 233 | expect(response2.status).to.equal(StatusCodes.OK); 234 | }); 235 | 236 | it('throttles 90% of dev errors', async () => { 237 | sandbox.stub(Math, 'random').returns(0.1); 238 | const response1 = await makeRequest(referrer, knownGoodQuery); 239 | expect(response1.status).to.equal(StatusCodes.ACCEPTED); 240 | 241 | Math.random.returns(0.11); 242 | const response2 = await makeRequest(referrer, knownGoodQuery); 243 | expect(response2.status).to.equal(StatusCodes.OK); 244 | }); 245 | 246 | it('throttles 99% of user errors', async () => { 247 | const query = Object.assign({}, knownGoodQuery, { assert: true }); 248 | 249 | sandbox.stub(Math, 'random').returns(0.01); 250 | const response1 = await makeRequest(referrer, query); 251 | expect(response1.status).to.equal(StatusCodes.ACCEPTED); 252 | 253 | Math.random.returns(0.02); 254 | const response2 = await makeRequest(referrer, query); 255 | expect(response2.status).to.equal(StatusCodes.OK); 256 | }); 257 | }); 258 | 259 | describe('logging errors', () => { 260 | beforeEach(() => { 261 | sandbox.stub(Math, 'random').returns(0); 262 | }); 263 | 264 | describe('empty stack traces', () => { 265 | const query = Object.assign({}, knownGoodQuery, { 266 | stack: '', 267 | debug: true, 268 | }); 269 | 270 | it('logs http request', async () => { 271 | const { body } = await makeRequest(referrer, query); 272 | const { httpRequest } = body.event.context; 273 | expect(httpRequest.url).to.be.equal( 274 | '/r?v=011830043289240&m=The%20object%20does%20' + 275 | 'not%20support%20the%20operation%20or%20argument.&a=0&rt=1p' + 276 | '&bt=production&s=&debug=1' 277 | ); 278 | expect(httpRequest.userAgent).to.be.equal(userAgent); 279 | expect(httpRequest.referrer).to.be.equal(referrer); 280 | }); 281 | 282 | it('logs missing stack trace', async () => { 283 | const { body } = await makeRequest(referrer, query); 284 | expect(body.event.message).to.be.equal( 285 | `Error: ${query.message}\n at ` + 286 | 'the-object-does-not-support-the-operation-or-argument.js:1:1' 287 | ); 288 | }); 289 | }); 290 | 291 | describe('safari stack traces', () => { 292 | const query = Object.assign({}, knownGoodQuery, { 293 | stack: 294 | 't@https://cdn.ampproject.org/v0.js:1:18\n' + 295 | 'https://cdn.ampproject.org/v0.js:2:18', 296 | debug: true, 297 | }); 298 | 299 | it('logs http request', async () => { 300 | const { body } = await makeRequest(referrer, query); 301 | const { httpRequest } = body.event.context; 302 | expect(httpRequest.url).to.be.equal( 303 | '/r?v=011830043289240&m=The%20object%20does%20' + 304 | 'not%20support%20the%20operation%20or%20argument.&a=0&rt=1p' + 305 | '&bt=production' + 306 | '&s=t%40https%3A%2F%2Fcdn.ampproject.org%2Fv0.js%3A1%3A18%0A' + 307 | 'https%3A%2F%2Fcdn.ampproject.org%2Fv0.js%3A2%3A18&debug=1' 308 | ); 309 | expect(httpRequest.userAgent).to.be.equal(userAgent); 310 | expect(httpRequest.referrer).to.be.equal(referrer); 311 | }); 312 | 313 | describe('when unminification fails', () => { 314 | it('logs full error', async () => { 315 | const { body } = await makeRequest(referrer, query); 316 | expect(body.event.message).to.be.equal( 317 | 'Error: The object does not support the operation or argument.\n' + 318 | ' at t (https://cdn.ampproject.org/v0.js:1:18)\n' + 319 | ' at https://cdn.ampproject.org/v0.js:2:18' 320 | ); 321 | }); 322 | }); 323 | 324 | describe('when unminification succeeds', () => { 325 | beforeEach(() => { 326 | nock('https://cdn.ampproject.org') 327 | .get('/rtv/metadata') 328 | .reply(200, { 329 | ampRuntimeVersion: '011830043289240', 330 | diversions: ['001830043289240'], 331 | }) 332 | .get('/rtv/011830043289240/v0.js.map') 333 | .reply(200, rawSourceMap); 334 | }); 335 | 336 | it('logs full error', async () => { 337 | const { body } = await makeRequest(referrer, query); 338 | expect(body.event.message).to.be.equal( 339 | 'Error: The object does not support the operation or argument.\n' + 340 | ' at bar (https://cdn.ampproject.org/one.js:1:21)\n' + 341 | ' at n (https://cdn.ampproject.org/two.js:1:21)' 342 | ); 343 | }); 344 | }); 345 | }); 346 | 347 | describe('chrome stack traces', () => { 348 | const query = Object.assign({}, knownGoodQuery, { 349 | stack: 350 | `${knownGoodQuery.message}\n` + 351 | ' at t (https://cdn.ampproject.org/v0.js:1:18)\n' + 352 | ' at https://cdn.ampproject.org/v0.js:2:18', 353 | debug: true, 354 | }); 355 | 356 | it('logs http request', async () => { 357 | const { body } = await makeRequest(referrer, query); 358 | const { httpRequest } = body.event.context; 359 | expect(httpRequest.url).to.be.equal( 360 | '/r?v=011830043289240&m=The%20object%20does%20' + 361 | 'not%20support%20the%20operation%20or%20argument.&a=0&rt=1p' + 362 | '&bt=production' + 363 | '&s=The%20object%20does%20not%20support%20the%20operation%20or' + 364 | '%20argument.%0A%20%20%20%20at%20t%20(https%3A%2F%2Fcdn.ampproject.org' + 365 | '%2Fv0.js%3A1%3A18)%0A%20%20%20%20at%20https%3A%2F%2Fcdn.ampproject.' + 366 | 'org%2Fv0.js%3A2%3A18&debug=1' 367 | ); 368 | expect(httpRequest.userAgent).to.be.equal(userAgent); 369 | expect(httpRequest.referrer).to.be.equal(referrer); 370 | }); 371 | 372 | describe('when unminification fails', () => { 373 | it('logs full error', async () => { 374 | const { body } = await makeRequest(referrer, query); 375 | expect(body.event.message).to.be.equal( 376 | 'Error: The object does not support the operation or argument.\n' + 377 | ' at t (https://cdn.ampproject.org/v0.js:1:18)\n' + 378 | ' at https://cdn.ampproject.org/v0.js:2:18' 379 | ); 380 | }); 381 | }); 382 | 383 | describe('when unminification succeeds', () => { 384 | beforeEach(() => { 385 | nock('https://cdn.ampproject.org') 386 | .get('/rtv/metadata') 387 | .reply(200, { 388 | ampRuntimeVersion: '011830043289240', 389 | diversions: ['001830043289240'], 390 | }) 391 | .get('/rtv/011830043289240/v0.js.map') 392 | .reply(200, rawSourceMap); 393 | }); 394 | 395 | it('logs full error', async () => { 396 | const { body } = await makeRequest(referrer, query); 397 | expect(body.event.message).to.be.equal( 398 | 'Error: The object does not support the operation or argument.\n' + 399 | ' at bar (https://cdn.ampproject.org/one.js:1:21)\n' + 400 | ' at n (https://cdn.ampproject.org/two.js:1:21)' 401 | ); 402 | }); 403 | }); 404 | }); 405 | }); 406 | }); 407 | } 408 | }); 409 | -------------------------------------------------------------------------------- /test/unit/test-cache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Cache } from '../../utils/cache.js'; 18 | 19 | describe('Cache cleans up unused entries periodically', () => { 20 | let sandbox; 21 | let clock; 22 | 23 | beforeEach(() => { 24 | sandbox = sinon.createSandbox({ 25 | useFakeTimers: true, 26 | }); 27 | clock = sandbox.clock; 28 | }); 29 | afterEach(() => { 30 | sandbox.restore(); 31 | }); 32 | 33 | class Consumer {} 34 | class DestroyConsumer { 35 | destroy() {} 36 | } 37 | 38 | it('Should delete entry that has not been accessed in wait ms', () => { 39 | const cacheMap = new Cache(10); 40 | cacheMap.set(4, new Consumer()); 41 | 42 | clock.tick(9); 43 | expect(cacheMap.size).to.equal(1); 44 | 45 | clock.tick(1); 46 | expect(cacheMap.size).to.equal(0); 47 | }); 48 | 49 | it('Should reset lifetime of entry if accessed before wait ms', () => { 50 | const cacheMap = new Cache(10); 51 | cacheMap.set(4, new Consumer()); 52 | 53 | clock.tick(9); 54 | expect(cacheMap.size).to.equal(1); 55 | 56 | cacheMap.get(4); 57 | clock.tick(1); 58 | expect(cacheMap.size).to.equal(1); 59 | }); 60 | 61 | it('Should delete an entry that has been accessed after expiry', () => { 62 | const cacheMap = new Cache(10); 63 | cacheMap.set(4, new Consumer()); 64 | 65 | clock.tick(10); 66 | expect(cacheMap.get(4)).to.equal(undefined); 67 | }); 68 | 69 | it('Should call destroy on the entry after expiry if has destroy', () => { 70 | const cacheMap = new Cache(10); 71 | const consumer = new Consumer(); 72 | const dconsumer = new DestroyConsumer(); 73 | const spy = sandbox.spy(dconsumer, 'destroy'); 74 | 75 | cacheMap.set(4, consumer); 76 | cacheMap.set(5, dconsumer); 77 | clock.tick(10); 78 | 79 | expect(spy.called).to.be.true; 80 | }); 81 | 82 | it('Should delete after maxWait ms', () => { 83 | const cacheMap = new Cache(10, 30); 84 | cacheMap.set(4, new Consumer()); 85 | 86 | clock.tick(9); 87 | cacheMap.get(4); 88 | clock.tick(9); 89 | cacheMap.get(4); 90 | clock.tick(9); 91 | cacheMap.get(4); 92 | expect(cacheMap.size).to.equal(1); 93 | 94 | clock.tick(2); 95 | expect(cacheMap.size).to.equal(1); 96 | 97 | clock.tick(1); 98 | expect(cacheMap.size).to.equal(0); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/unit/test-frame.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Frame } from '../../utils/stacktrace/frame.js'; 18 | 19 | describe('Frame', () => { 20 | describe('#toString', () => { 21 | describe('with context name', () => { 22 | it('includes context with parenthesis around location', () => { 23 | const f = new Frame('name', 'file.js', '1', '2'); 24 | expect(f.toString()).to.equal(' at name (file.js:1:2)'); 25 | }); 26 | }); 27 | 28 | describe('without context name', () => { 29 | it('includes location without parenthesis', () => { 30 | const f = new Frame('', 'file.js', '1', '2'); 31 | expect(f.toString()).to.equal(' at file.js:1:2'); 32 | }); 33 | }); 34 | }); 35 | 36 | describe('path deduplication', () => { 37 | [ 38 | [ 39 | 'https://raw.githubusercontent.com/ampproject/amphtml/2004142326360/extensions/amp-viewer-integration/0.1/messaging/extensions/amp-viewer-integration/0.1/messaging/messaging.js', 40 | 'https://raw.githubusercontent.com/ampproject/amphtml/2004142326360/extensions/amp-viewer-integration/0.1/messaging/messaging.js', 41 | ], 42 | [ 43 | 'https://raw.githubusercontent.com/ampproject/amphtml/2004071640410/src/service/src/service/storage-impl.js', 44 | 'https://raw.githubusercontent.com/ampproject/amphtml/2004071640410/src/service/storage-impl.js', 45 | ], 46 | [ 47 | 'https://raw.githubusercontent.com/ampproject/amphtml/2004071640410/src/src/chunk.js', 48 | 'https://raw.githubusercontent.com/ampproject/amphtml/2004071640410/src/chunk.js', 49 | ], 50 | [ 51 | 'https://raw.githubusercontent.com/ampproject/amphtml/2004071640410/src/src-subdir/chunk.js', 52 | 'https://raw.githubusercontent.com/ampproject/amphtml/2004071640410/src/src-subdir/chunk.js', 53 | ], 54 | ['unexpected-token.js', 'unexpected-token.js'], 55 | ].forEach(([duped, fixed], index) => { 56 | it(`fixes duplicated source paths: ${index}`, () => { 57 | const frame = new Frame('name', duped, '1', '2'); 58 | expect(frame.source).to.equal(fixed); 59 | }); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/unit/test-human-rtv.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS-IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { humanRtv } from '../../utils/rtv/human-rtv.js'; 16 | 17 | describe('humanRtv', () => { 18 | [ 19 | ['052004030010070', '04-03 Nightly-Control (0010+70)'], 20 | ['012004030010000', '04-03 Stable (0010)'], 21 | ['012004030010001', '04-03 Stable (0010+1)'], 22 | ['012004030010002', '04-03 Stable (0010+2)'], 23 | ['022004030010070', '04-03 Control (0010+70)'], 24 | ['002004172112280', '04-17 Experimental (2112+280)'], 25 | ['032004172112280', '04-17 Beta (2112+280)'], 26 | ['042004210608300', '04-21 Nightly (0608+300)'], 27 | ['internalRuntimeVersion', 'internalRuntimeVersion'], 28 | ].forEach(([rtv, str]) => { 29 | it(`converts "${rtv}" to "${str}"`, () => { 30 | expect(humanRtv(rtv)).to.equal(str); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/unit/test-log-target.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS-IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import * as logs from '../../utils/log.js'; 16 | import { LoggingTarget } from '../../utils/log-target.js'; 17 | 18 | describe('log target', () => { 19 | let sandbox; 20 | let referrer; 21 | let reportingParams; 22 | 23 | beforeEach(() => { 24 | sandbox = sinon.createSandbox(); 25 | referrer = 'https://my.awesomesite.com'; 26 | reportingParams = { 27 | assert: false, 28 | binaryType: 'production', 29 | canary: false, 30 | debug: false, 31 | expected: false, 32 | message: 'Error: Something is borked!', 33 | queryString: '[query string]', 34 | runtime: '1p', 35 | singlePassType: undefined, 36 | stacktrace: 'Error: Something is borked!\n at Error()', 37 | thirdParty: false, 38 | version: '012004030010002', 39 | }; 40 | }); 41 | 42 | afterEach(() => { 43 | sandbox.restore(); 44 | }); 45 | 46 | describe('log', () => { 47 | it('returns error log', async () => { 48 | const logTarget = new LoggingTarget(referrer, reportingParams); 49 | 50 | expect(logTarget.log).to.equal(logs.errors); 51 | }); 52 | 53 | it('returns ads log for inabox', async () => { 54 | reportingParams.runtime = 'inabox'; 55 | const logTarget = new LoggingTarget(referrer, reportingParams); 56 | 57 | expect(logTarget.log).to.equal(logs.ads); 58 | }); 59 | 60 | it('returns ads log for signing service error', async () => { 61 | reportingParams.message = 'Error: Signing service error for google'; 62 | const logTarget = new LoggingTarget(referrer, reportingParams); 63 | 64 | expect(logTarget.log).to.equal(logs.ads); 65 | }); 66 | 67 | it('returns user log for asserts', async () => { 68 | reportingParams.assert = true; 69 | const logTarget = new LoggingTarget(referrer, reportingParams); 70 | 71 | expect(logTarget.log).to.equal(logs.users); 72 | }); 73 | 74 | it('returns expected log for expected errors', async () => { 75 | reportingParams.expected = true; 76 | const logTarget = new LoggingTarget(referrer, reportingParams); 77 | 78 | expect(logTarget.log).to.equal(logs.expected); 79 | }); 80 | }); 81 | 82 | describe('serviceName', () => { 83 | describe('referrer split', () => { 84 | [ 85 | 'https://cdn.ampproject.org/mywebsite.com/index.html', 86 | 'https://mywebsite-com.cdn.ampproject.org/index.html', 87 | 'https://mywebsite-com.ampproject.net/index.html', 88 | ].forEach((referrer) => { 89 | it(`correctly records "Google Cache" for referrer ${referrer}`, () => { 90 | const logTarget = new LoggingTarget(referrer, reportingParams); 91 | expect(logTarget.serviceName).to.contain('Google Cache'); 92 | expect(logTarget.serviceName).to.not.contain('Publisher Origin'); 93 | }); 94 | }); 95 | 96 | ['https://mywebsite.com/index.html', 'https://amp.dev/'].forEach( 97 | (referrer) => { 98 | it(`correctly records "Publisher Origin" for referrer ${referrer}`, () => { 99 | const logTarget = new LoggingTarget(referrer, reportingParams); 100 | expect(logTarget.serviceName).to.contain('Publisher Origin'); 101 | expect(logTarget.serviceName).to.not.contain('Google Cache'); 102 | }); 103 | } 104 | ); 105 | }); 106 | 107 | describe('for origin referrers', () => { 108 | const serviceParams = [ 109 | ['1%', '00XXXXXXXXXXXXX'], 110 | ['1%', '03XXXXXXXXXXXXX'], 111 | ['Production', '01XXXXXXXXXXXXX'], 112 | ['Production', '02XXXXXXXXXXXXX'], 113 | ['Nightly', '04XXXXXXXXXXXXX'], 114 | ['Nightly', '05XXXXXXXXXXXXX'], 115 | ['Experiments', '10XXXXXXXXXXXXX'], 116 | ['Experiments', '11XXXXXXXXXXXXX'], 117 | ['Experiments', '12XXXXXXXXXXXXX'], 118 | ['Inabox-Control-A', '20XXXXXXXXXXXXX'], 119 | ['Inabox-Experiment-A', '21XXXXXXXXXXXXX'], 120 | ['Inabox-Control-B', '22XXXXXXXXXXXXX'], 121 | ['Inabox-Experiment-B', '23XXXXXXXXXXXXX'], 122 | ['Inabox-Control-C', '24XXXXXXXXXXXXX'], 123 | ['Inabox-Experiment-C', '25XXXXXXXXXXXXX'], 124 | ]; 125 | 126 | for (const [expectedName, version] of serviceParams) { 127 | it(`correctly constructs service name for "${expectedName} (${version})"`, () => { 128 | const logTarget = new LoggingTarget( 129 | referrer, 130 | Object.assign(reportingParams, { 131 | version, 132 | cdn: 'cdn.ampproject.org', 133 | }) 134 | ); 135 | 136 | expect(logTarget.serviceName).to.equal( 137 | `${expectedName} > Publisher Origin (cdn.ampproject.org)` 138 | ); 139 | }); 140 | } 141 | 142 | it('correctly constructs service name for expected errors', () => { 143 | const logTarget = new LoggingTarget( 144 | referrer, 145 | Object.assign(reportingParams, { 146 | assert: true, 147 | expected: true, 148 | cdn: 'cdn.ampproject.org', 149 | }) 150 | ); 151 | 152 | expect(logTarget.serviceName).to.equal( 153 | 'Production > Publisher Origin (cdn.ampproject.org) > (Expected)' 154 | ); 155 | }); 156 | 157 | ['cdn.ampproject.org', 'ampjs.org'].forEach((cdn) => { 158 | it(`correctly constructs service name for JS served from ${cdn}`, () => { 159 | const logTarget = new LoggingTarget( 160 | referrer, 161 | Object.assign(reportingParams, { cdn }) 162 | ); 163 | 164 | expect(logTarget.serviceName).to.equal( 165 | `Production > Publisher Origin (${cdn})` 166 | ); 167 | }); 168 | }); 169 | }); 170 | 171 | it('correctly constructs service name for origin pages with unreported JS CDN', () => { 172 | const logTarget = new LoggingTarget(referrer, reportingParams); 173 | 174 | expect(logTarget.serviceName).to.equal( 175 | 'Production > Publisher Origin (CDN not reported)' 176 | ); 177 | }); 178 | }); 179 | 180 | describe('versionId', () => { 181 | it('returns the release version string', () => { 182 | const logTarget = new LoggingTarget(referrer, reportingParams); 183 | expect(logTarget.versionId).to.equal('04-03 Stable (0010+2)'); 184 | }); 185 | }); 186 | 187 | describe('throttleRate', () => { 188 | beforeEach(() => { 189 | referrer = 'https://cdn.ampproject.org/mywebsite.com/index.html'; 190 | }); 191 | 192 | it('throttles Stable by a factor of 10', () => { 193 | const logTarget = new LoggingTarget(referrer, reportingParams); 194 | expect(logTarget.throttleRate).to.be.closeTo(1 / 10, 1e-6); 195 | }); 196 | 197 | it('does not throttle canary', () => { 198 | reportingParams.canary = true; 199 | const logTarget = new LoggingTarget(referrer, reportingParams); 200 | expect(logTarget.throttleRate).to.be.closeTo(1, 1e-6); 201 | }); 202 | 203 | it('does not throttle Control', () => { 204 | reportingParams.binaryType = 'control'; 205 | const logTarget = new LoggingTarget(referrer, reportingParams); 206 | expect(logTarget.throttleRate).to.be.closeTo(1, 1e-6); 207 | }); 208 | 209 | it('does not throttle RC', () => { 210 | reportingParams.binaryType = 'rc'; 211 | const logTarget = new LoggingTarget(referrer, reportingParams); 212 | expect(logTarget.throttleRate).to.be.closeTo(1, 1e-6); 213 | }); 214 | 215 | it('does not throttle Nightly', () => { 216 | reportingParams.binaryType = 'nightly'; 217 | const logTarget = new LoggingTarget(referrer, reportingParams); 218 | expect(logTarget.throttleRate).to.be.closeTo(1, 1e-6); 219 | }); 220 | 221 | it('throttles user errors by a factor of 10', () => { 222 | reportingParams.assert = true; 223 | const logTarget = new LoggingTarget(referrer, reportingParams); 224 | expect(logTarget.throttleRate).to.be.closeTo(1 / 100, 1e-6); 225 | }); 226 | 227 | it('throttles errors from origin pages by a factor of 10', () => { 228 | referrer = 'https://myrandomwebsite.com'; 229 | const logTarget = new LoggingTarget(referrer, reportingParams); 230 | expect(logTarget.throttleRate).to.be.closeTo(1 / 10, 1e-6); 231 | }); 232 | 233 | it('throttles expected errors by a factor of 10', () => { 234 | reportingParams.expected = true; 235 | const logTarget = new LoggingTarget(referrer, reportingParams); 236 | expect(logTarget.throttleRate).to.be.closeTo(1 / 100, 1e-6); 237 | }); 238 | 239 | it('throttles expected user errors in RC on origin by 100', () => { 240 | referrer = 'https://myrandomwebsite.com'; 241 | reportingParams.assert = true; 242 | reportingParams.binaryType = 'rc'; 243 | reportingParams.expected = true; 244 | const logTarget = new LoggingTarget(referrer, reportingParams); 245 | expect(logTarget.throttleRate).to.be.closeTo(1 / 100, 1e-6); 246 | }); 247 | }); 248 | }); 249 | -------------------------------------------------------------------------------- /test/unit/test-query-string.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { stringify } from '../../utils/requests/query-string.js'; 18 | 19 | describe('Query String', () => { 20 | describe('stringify', () => { 21 | it('builds query string', () => { 22 | const result = stringify({ test: 'value' }); 23 | expect(result).to.equal('test=value'); 24 | }); 25 | 26 | it('does not prepend "?"', () => { 27 | const result = stringify({ test: 'value' }); 28 | expect(result.startsWith('?')).to.equal(false); 29 | }); 30 | 31 | it('joins multiple params with "&"', () => { 32 | const result = stringify({ test: 'value', next: 'next' }); 33 | expect(result).to.equal('test=value&next=next'); 34 | }); 35 | 36 | it('leaves empty value for empty strings', () => { 37 | const result = stringify({ test: '' }); 38 | expect(result).to.equal('test='); 39 | }); 40 | 41 | it('encodes keys', () => { 42 | const result = stringify({ 'te st': 'value' }); 43 | expect(result).to.equal('te%20st=value'); 44 | }); 45 | 46 | it('encodes values', () => { 47 | const result = stringify({ test: 'val ue' }); 48 | expect(result).to.equal('test=val%20ue'); 49 | }); 50 | 51 | it('iterates enumerable properties', () => { 52 | const obj = { test: 'value' }; 53 | Object.defineProperty(obj, 'next', { 54 | value: 'next', 55 | }); 56 | 57 | const result = stringify(obj); 58 | expect(result).to.equal('test=value'); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/unit/test-should-ignore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { shouldIgnore } from '../../utils/stacktrace/should-ignore.js'; 18 | import { Frame } from '../../utils/stacktrace/frame.js'; 19 | 20 | describe('shouldIgnore', () => { 21 | const jsFrame = new Frame('', 'file.js', '1', '2'); 22 | const htmlFrame = new Frame('', 'file.html', '1', '2'); 23 | const mjsFrame = new Frame('', 'file.mjs', '1', '2'); 24 | 25 | const jsFrames = [jsFrame, jsFrame]; 26 | const mjsFrames = [mjsFrame, mjsFrame]; 27 | const mixedJsFrames = [jsFrame, mjsFrame, jsFrame]; 28 | const mixedFrames = [jsFrame, htmlFrame, jsFrame]; 29 | const htmlFrames = [htmlFrame, htmlFrame]; 30 | 31 | describe('with acceptable error message', () => { 32 | const message = 'Error: something happened!'; 33 | 34 | it('does not ignore js frames', () => { 35 | expect(shouldIgnore(message, jsFrames)).to.equal(false); 36 | }); 37 | 38 | it('does not ignore mjs frames', () => { 39 | expect(shouldIgnore(message, mjsFrames)).to.equal(false); 40 | }); 41 | 42 | it('does not ignore mixed js and mjs frames', () => { 43 | expect(shouldIgnore(message, mixedJsFrames)).to.equal(false); 44 | }); 45 | 46 | it('ignores mixed frames', () => { 47 | expect(shouldIgnore(message, mixedFrames)).to.equal(true); 48 | }); 49 | 50 | it('ignores html frames', () => { 51 | expect(shouldIgnore(message, htmlFrames)).to.equal(true); 52 | }); 53 | }); 54 | 55 | describe('with blacklisted error message', () => { 56 | [ 57 | 'stop_youtube', 58 | 'null%20is%20not%20an%20object%20(evaluating%20%27elt.parentNode%27)', 59 | ].forEach((message) => { 60 | describe(`"${message}"`, () => { 61 | it('ignores js frames', () => { 62 | expect(shouldIgnore(message, jsFrames)).to.equal(true); 63 | }); 64 | 65 | it('ignores mixed frames', () => { 66 | expect(shouldIgnore(message, mixedFrames)).to.equal(true); 67 | }); 68 | 69 | it('ignores html frames', () => { 70 | expect(shouldIgnore(message, htmlFrames)).to.equal(true); 71 | }); 72 | }); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/unit/test-standardize-stack-trace.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { standardizeStackTrace } from '../../utils/stacktrace/standardize-stack-trace.js'; 18 | 19 | describe('standardizeStackTrace', () => { 20 | describe('with a Chrome stack trace', () => { 21 | const frames = standardizeStackTrace( 22 | `Error: localStorage not supported. 23 | at Error (native) 24 | at new vi (https://cdn.ampproject.org/rtv/031496877433269/v0.js:297:149) 25 | at https://cdn.ampproject.org/rtv/031496877433269/v0.js:298:365 26 | at dc (https://cdn.ampproject.org/rtv/031496877433269/v0.js:53:59) 27 | at I (https://cdn.ampproject.org/rtv/031496877433269/v0.js:51:626) 28 | at xi (https://cdn.ampproject.org/rtv/031496877433269/v0.js:298:278) 29 | at mf.zc (https://cdn.ampproject.org/rtv/031496877433269/v0.js:408:166) 30 | at pf (https://cdn.ampproject.org/rtv/031496877433269/v0.js:112:409) 31 | at lf.$d (https://cdn.ampproject.org/rtv/031496877433269/v0.js:115:86) 32 | at https://cdn.ampproject.org/rtv/031496877433269/v0.js:114:188`, 33 | 'Error: localStorage not supported.' 34 | ); 35 | 36 | it('normalizes into 9 frames', () => { 37 | expect(frames).to.have.length(9); 38 | }); 39 | 40 | it('extracts name context', () => { 41 | expect(frames[0].name).to.equal('new vi'); 42 | expect(frames[2].name).to.equal('dc'); 43 | expect(frames[3].name).to.equal('I'); 44 | expect(frames[4].name).to.equal('xi'); 45 | expect(frames[5].name).to.equal('mf.zc'); 46 | expect(frames[6].name).to.equal('pf'); 47 | expect(frames[7].name).to.equal('lf.$d'); 48 | }); 49 | 50 | it('extracts nameless frames', () => { 51 | expect(frames[1].name).to.equal(''); 52 | expect(frames[8].name).to.equal(''); 53 | }); 54 | 55 | it('extracts source locations', () => { 56 | for (let i = 0; i < frames.length; i++) { 57 | expect(frames[i].source).to.equal( 58 | 'https://cdn.ampproject.org/rtv/031496877433269/v0.js', 59 | `frame ${i}` 60 | ); 61 | } 62 | }); 63 | 64 | it('extracts line numbers', () => { 65 | expect(frames[0].line).to.equal(297); 66 | expect(frames[1].line).to.equal(298); 67 | expect(frames[2].line).to.equal(53); 68 | expect(frames[3].line).to.equal(51); 69 | expect(frames[4].line).to.equal(298); 70 | expect(frames[5].line).to.equal(408); 71 | expect(frames[6].line).to.equal(112); 72 | expect(frames[7].line).to.equal(115); 73 | expect(frames[8].line).to.equal(114); 74 | }); 75 | 76 | it('extracts column numbers', () => { 77 | expect(frames[0].column).to.equal(149); 78 | expect(frames[1].column).to.equal(365); 79 | expect(frames[2].column).to.equal(59); 80 | expect(frames[3].column).to.equal(626); 81 | expect(frames[4].column).to.equal(278); 82 | expect(frames[5].column).to.equal(166); 83 | expect(frames[6].column).to.equal(409); 84 | expect(frames[7].column).to.equal(86); 85 | expect(frames[8].column).to.equal(188); 86 | }); 87 | 88 | it('normalizes .js.br files to .js', () => { 89 | const frames = standardizeStackTrace(`Error: message 90 | at new v1 (https://cdn.ampproject.org/rtv/031496877433269/v0.js:297:149) 91 | at new v2 (https://cdn.ampproject.org/rtv/031496877433269/v0.js.br:297:149) 92 | `); 93 | expect(frames[0].source).to.equal( 94 | 'https://cdn.ampproject.org/rtv/031496877433269/v0.js' 95 | ); 96 | expect(frames[1].source).to.equal( 97 | 'https://cdn.ampproject.org/rtv/031496877433269/v0.js' 98 | ); 99 | }); 100 | }); 101 | 102 | describe('with a Safari stack trace', () => { 103 | const frames = standardizeStackTrace( 104 | `Zd@https://cdn.ampproject.org/v0.js:5:204 105 | error@https://cdn.ampproject.org/v0.js:5:314 106 | jh@https://cdn.ampproject.org/v0.js:237:205 107 | dc@https://cdn.ampproject.org/v0.js:53:69 108 | I@https://cdn.ampproject.org/v0.js:51:628 109 | https://cdn.ampproject.org/v0.js:408:173 110 | pf@https://cdn.ampproject.org/v0.js:112:411 111 | $d@https://cdn.ampproject.org/v0.js:115:88 112 | [native code] 113 | https://cdn.ampproject.org/v0.js:115:170 114 | promiseReactionJob@[native code]`, 115 | 'Error doing something' 116 | ); 117 | 118 | it('normalizes into 9 frames', () => { 119 | expect(frames).to.have.length(9); 120 | }); 121 | 122 | it('extracts name context', () => { 123 | expect(frames[0].name).to.equal('Zd'); 124 | expect(frames[1].name).to.equal('error'); 125 | expect(frames[2].name).to.equal('jh'); 126 | expect(frames[3].name).to.equal('dc'); 127 | expect(frames[4].name).to.equal('I'); 128 | expect(frames[6].name).to.equal('pf'); 129 | expect(frames[7].name).to.equal('$d'); 130 | }); 131 | 132 | it('extracts nameless frames', () => { 133 | expect(frames[5].name).to.equal(''); 134 | expect(frames[8].name).to.equal(''); 135 | }); 136 | 137 | it('extracts source locations', () => { 138 | for (let i = 0; i < frames.length; i++) { 139 | expect(frames[i].source).to.equal( 140 | 'https://cdn.ampproject.org/v0.js', 141 | `frame ${i}` 142 | ); 143 | } 144 | }); 145 | 146 | it('extracts line numbers', () => { 147 | expect(frames[0].line).to.equal(5); 148 | expect(frames[1].line).to.equal(5); 149 | expect(frames[2].line).to.equal(237); 150 | expect(frames[3].line).to.equal(53); 151 | expect(frames[4].line).to.equal(51); 152 | expect(frames[5].line).to.equal(408); 153 | expect(frames[6].line).to.equal(112); 154 | expect(frames[7].line).to.equal(115); 155 | expect(frames[8].line).to.equal(115); 156 | }); 157 | 158 | it('extracts column numbers', () => { 159 | expect(frames[0].column).to.equal(204); 160 | expect(frames[1].column).to.equal(314); 161 | expect(frames[2].column).to.equal(205); 162 | expect(frames[3].column).to.equal(69); 163 | expect(frames[4].column).to.equal(628); 164 | expect(frames[5].column).to.equal(173); 165 | expect(frames[6].column).to.equal(411); 166 | expect(frames[7].column).to.equal(88); 167 | expect(frames[8].column).to.equal(170); 168 | }); 169 | 170 | it('normalizes .js.br files to .js', () => { 171 | const frames = standardizeStackTrace(`Error: message 172 | jh@https://cdn.ampproject.org/v0.js:237:205 173 | jh@https://cdn.ampproject.org/v0.js.br:237:205 174 | `); 175 | expect(frames[0].source).to.equal('https://cdn.ampproject.org/v0.js'); 176 | expect(frames[1].source).to.equal('https://cdn.ampproject.org/v0.js'); 177 | }); 178 | }); 179 | 180 | describe('empty stack traces', () => { 181 | it('inserts a missing frame for empty stacks', () => { 182 | const frames = standardizeStackTrace(``, 'Error: test'); 183 | expect(frames.length).to.equal(1); 184 | expect(frames[0].name).to.equal(''); 185 | expect(frames[0].source).to.equal('error-test.js'); 186 | expect(frames[0].line).to.equal(1); 187 | expect(frames[0].column).to.equal(1); 188 | }); 189 | 190 | it('generates unique filename based on message', () => { 191 | const frames = standardizeStackTrace(``, 'Daisy Daisy'); 192 | expect(frames.length).to.equal(1); 193 | expect(frames[0].source).to.equal('daisy-daisy.js'); 194 | }); 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /test/unit/test-unminify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Log } from '@google-cloud/logging'; 18 | import nock from 'nock'; 19 | 20 | import { 21 | unminify, 22 | normalizeCdnJsUrl, 23 | } from '../../utils/stacktrace/unminify.js'; 24 | import { Frame } from '../../utils/stacktrace/frame.js'; 25 | 26 | describe('unminify', () => { 27 | // https://github.com/mozilla/source-map/blob/75663e0187002920ad98ed1de21e54cb85114609/test/util.js#L161-L176 28 | const rawSourceMap = { 29 | version: 3, 30 | file: 'min.js', 31 | names: ['bar', 'baz', 'n'], 32 | sources: ['one.js', 'two.js'], 33 | sourcesContent: [ 34 | ' ONE.foo = function (bar) {\n return baz(bar);\n };', 35 | ' TWO.inc = function (n) {\n return n + 1;\n };', 36 | ], 37 | sourceRoot: 'https://cdn.ampproject.org', 38 | mappings: 39 | 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;' + 40 | 'CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA', 41 | }; 42 | const frame1 = new Frame( 43 | 'foo', 44 | 'https://cdn.ampproject.org/v0.js', 45 | '1', 46 | '18' 47 | ); 48 | const frame2 = new Frame( 49 | 'bar', 50 | 'https://cdn.ampproject.org/v0.js', 51 | '1', 52 | '24' 53 | ); 54 | const frame3 = new Frame( 55 | 'baz', 56 | 'https://cdn.ampproject.org/v1.js', 57 | '2', 58 | '18' 59 | ); 60 | const versionedFrame = new Frame( 61 | 'test', 62 | 'https://cdn.ampproject.org/rtv/001502924683165/v0.js', 63 | '1', 64 | '2' 65 | ); 66 | const nonCdnFrame = new Frame('test', 'http://other.com/v0.js', '1', '2'); 67 | const moduleFrame = new Frame( 68 | 'foo', 69 | 'https://cdn.ampproject.org/v0-module.js', 70 | '1', 71 | '18' 72 | ); 73 | const nomoduleFrame = new Frame( 74 | 'foo', 75 | 'https://cdn.ampproject.org/v0-nomodule.js', 76 | '1', 77 | '18' 78 | ); 79 | 80 | /** @type {sinon.SinonSandbox} */ 81 | let sandbox; 82 | /** @type {sinon.SinonFakeTimers} */ 83 | let clock; 84 | 85 | beforeEach(() => { 86 | nock.disableNetConnect(); 87 | nock.enableNetConnect('127.0.0.1'); 88 | 89 | sandbox = sinon.createSandbox({ 90 | useFakeTimers: true, 91 | }); 92 | clock = sandbox.clock; 93 | sandbox.stub(Log.prototype, 'write').resolves(); 94 | }); 95 | 96 | afterEach(async () => { 97 | // Expired all cached sourcemaps 98 | await clock.tickAsync(1e10); 99 | 100 | sandbox.restore(); 101 | 102 | expect(nock.pendingMocks()).to.be.empty; 103 | nock.cleanAll(); 104 | }); 105 | 106 | it('unminifies multiple frames (same file)', async () => { 107 | nock('https://cdn.ampproject.org') 108 | .get('/rtv/123/v0.js.map') 109 | .reply(200, rawSourceMap); 110 | 111 | const [f1, f2] = await unminify([frame1, frame2], '123'); 112 | expect(f1.source).to.equal('https://cdn.ampproject.org/one.js'); 113 | expect(f1.line).to.equal(1); 114 | expect(f1.column).to.equal(21); 115 | expect(f2.source).to.equal('https://cdn.ampproject.org/one.js'); 116 | expect(f2.line).to.equal(2); 117 | expect(f2.column).to.equal(3); 118 | }); 119 | 120 | it('unminifies multiple frames (multiple files)', async () => { 121 | nock('https://cdn.ampproject.org') 122 | .get('/rtv/123/v0.js.map') 123 | .reply(200, rawSourceMap) 124 | .get('/rtv/123/v1.js.map') 125 | .reply(200, rawSourceMap); 126 | 127 | const [f1, f2] = await unminify([frame1, frame3], '123'); 128 | expect(f1.source).to.equal('https://cdn.ampproject.org/one.js'); 129 | expect(f1.line).to.equal(1); 130 | expect(f1.column).to.equal(21); 131 | expect(f2.source).to.equal('https://cdn.ampproject.org/two.js'); 132 | expect(f2.line).to.equal(1); 133 | expect(f2.column).to.equal(21); 134 | }); 135 | 136 | it('is resilant to sourcemap fetches failing', async () => { 137 | nock('https://cdn.ampproject.org') 138 | .get('/rtv/123/v0.js.map') 139 | .reply(200, rawSourceMap) 140 | .get('/rtv/123/v1.js.map') 141 | .replyWithError('failure'); 142 | 143 | const [f1, f2] = await unminify([frame1, frame3], '123'); 144 | expect(f1.source).to.equal(frame1.source); 145 | expect(f1.line).to.equal(frame1.line); 146 | expect(f1.column).to.equal(frame1.column); 147 | expect(f2.source).to.equal(frame3.source); 148 | expect(f2.line).to.equal(frame3.line); 149 | expect(f2.column).to.equal(frame3.column); 150 | }); 151 | 152 | it('does not unminify non-cdn js files', async () => { 153 | nock('https://cdn.ampproject.org') 154 | .get('/rtv/123/v0.js.map') 155 | .reply(200, rawSourceMap); 156 | 157 | const [f1, f2] = await unminify([frame1, nonCdnFrame], '123'); 158 | expect(f1.source).to.equal('https://cdn.ampproject.org/one.js'); 159 | expect(f1.line).to.equal(1); 160 | expect(f1.column).to.equal(21); 161 | expect(f2.source).to.equal(nonCdnFrame.source); 162 | expect(f2.line).to.equal(nonCdnFrame.line); 163 | expect(f2.column).to.equal(nonCdnFrame.column); 164 | }); 165 | 166 | it('does not request same file twice (same stack)', () => { 167 | nock('https://cdn.ampproject.org') 168 | .get('/rtv/123/v0.js.map') 169 | .reply(200, rawSourceMap); 170 | 171 | return unminify([frame1, frame2], '123'); 172 | }); 173 | 174 | it('does not request same file twice (consecutive stacks)', () => { 175 | nock('https://cdn.ampproject.org') 176 | .get('/rtv/123/v0.js.map') 177 | .reply(200, rawSourceMap); 178 | 179 | const p = unminify([frame1], '123'); 180 | const p2 = unminify([frame2], '123'); 181 | return Promise.all([p, p2]); 182 | }); 183 | 184 | it('does not request same file twice (after response)', async () => { 185 | nock('https://cdn.ampproject.org') 186 | .get('/rtv/123/v0.js.map') 187 | .reply(200, rawSourceMap); 188 | 189 | await unminify([frame1], '123'); 190 | return await unminify([frame2], '123'); 191 | }); 192 | 193 | it('requests file twice after purge', async () => { 194 | nock('https://cdn.ampproject.org') 195 | .get('/rtv/123/v0.js.map') 196 | .twice() 197 | .reply(200, rawSourceMap); 198 | 199 | await unminify([frame1], '123'); 200 | clock.tick(10000000000); 201 | return await unminify([frame2], '123'); 202 | }); 203 | 204 | it('normalizes unversioned files into rtv version', async () => { 205 | nock('https://cdn.ampproject.org') 206 | .get('/rtv/123/v0.js.map') 207 | .reply(200, rawSourceMap) 208 | .get('/rtv/124/v0.js.map') 209 | .reply(200, rawSourceMap) 210 | .get('/rtv/125/v0-module.js.map') 211 | .reply(200, rawSourceMap); 212 | 213 | await unminify([frame1], '123'); 214 | await unminify([frame2], '124'); 215 | return await unminify([moduleFrame], '125'); 216 | }); 217 | 218 | it('strips nomodule during normalization', () => { 219 | nock('https://cdn.ampproject.org') 220 | .get('/rtv/123/v0.js.map') 221 | .reply(200, rawSourceMap); 222 | 223 | return unminify([nomoduleFrame], '123'); 224 | }); 225 | 226 | it('does not normalize versioned files', () => { 227 | nock('https://cdn.ampproject.org') 228 | .get('/rtv/123/v0.js.map') 229 | .reply(200, rawSourceMap) 230 | .get('/rtv/001502924683165/v0.js.map') 231 | .reply(200, rawSourceMap); 232 | 233 | return unminify([frame1, versionedFrame], '123'); 234 | }); 235 | 236 | describe('URL normalization', () => { 237 | // Tests generated with: 238 | // const tests = [ 239 | // { 240 | // name: 'main binary', 241 | // tests: [ 242 | // { 243 | // input: 'v0.js', 244 | // expected: 'rtv/RTV123/v0.js', 245 | // }, 246 | // ], 247 | // }, 248 | // ]; 249 | // tests.map(({name, tests}) => { 250 | // const generated = tests.map(({input: inp, expected: exp}) => { 251 | // return (` 252 | // it('${inp}', () => { 253 | // const input = 'https://cdn.ampproject.org/${inp}'; 254 | // const expected = '${exp ? `https://cdn.ampproject.org/${exp}.map` : ''}'; 255 | 256 | // const actual = normalizeCdnJsUrl(input, 'RTV123'); 257 | // expect(actual).to.equal(expected); 258 | // }); 259 | // `); 260 | // }); 261 | 262 | // return (` 263 | // describe('${name}', () => { 264 | // ${generated.join('').trim()} 265 | // }); 266 | // `); 267 | // }).join(''); 268 | // 269 | // Parse the tests with: 270 | // const groups = s.match(/ describe\([^]*?\n }\);/g); 271 | // JSON.stringify(groups.map((group) => { 272 | // const name = group.match(/'(.*?)'/)[1]; 273 | // const tests = group.match(/it\([^}]*}\)/g).map((test) => { 274 | // const expected = test.match(/'(.*?)'/)[1]; 275 | // /input = 'https:\/\/cdn.ampproject.org\/(.*?)'/ 276 | // )[1]; 277 | // const expected = test.match( 278 | // /expected = '(?:https:\/\/cdn.ampproject.org\/(.*?).map)?'/ 279 | // )[1]; 280 | // return {input, expected}; 281 | // }); 282 | // return {name, tests}; 283 | // })); 284 | 285 | describe('main binary', () => { 286 | it('v0.js', () => { 287 | const input = 'https://cdn.ampproject.org/v0.js'; 288 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v0.js.map'; 289 | 290 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 291 | expect(actual).to.equal(expected); 292 | }); 293 | 294 | it('v0-module.js', () => { 295 | const input = 'https://cdn.ampproject.org/v0-module.js'; 296 | const expected = 297 | 'https://cdn.ampproject.org/rtv/RTV123/v0-module.js.map'; 298 | 299 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 300 | expect(actual).to.equal(expected); 301 | }); 302 | 303 | it('v0.js', () => { 304 | const input = 'https://cdn.ampproject.org/v0.js'; 305 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v0.js.map'; 306 | 307 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 308 | expect(actual).to.equal(expected); 309 | }); 310 | 311 | it('v1.js', () => { 312 | const input = 'https://cdn.ampproject.org/v1.js'; 313 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v1.js.map'; 314 | 315 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 316 | expect(actual).to.equal(expected); 317 | }); 318 | 319 | it('v1-module.js', () => { 320 | const input = 'https://cdn.ampproject.org/v1-module.js'; 321 | const expected = 322 | 'https://cdn.ampproject.org/rtv/RTV123/v1-module.js.map'; 323 | 324 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 325 | expect(actual).to.equal(expected); 326 | }); 327 | 328 | it('v1.js', () => { 329 | const input = 'https://cdn.ampproject.org/v1.js'; 330 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v1.js.map'; 331 | 332 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 333 | expect(actual).to.equal(expected); 334 | }); 335 | 336 | it('v20.js', () => { 337 | const input = 'https://cdn.ampproject.org/v20.js'; 338 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v20.js.map'; 339 | 340 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 341 | expect(actual).to.equal(expected); 342 | }); 343 | 344 | it('v20-module.js', () => { 345 | const input = 'https://cdn.ampproject.org/v20-module.js'; 346 | const expected = 347 | 'https://cdn.ampproject.org/rtv/RTV123/v20-module.js.map'; 348 | 349 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 350 | expect(actual).to.equal(expected); 351 | }); 352 | 353 | it('v20.js', () => { 354 | const input = 'https://cdn.ampproject.org/v20.js'; 355 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v20.js.map'; 356 | 357 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 358 | expect(actual).to.equal(expected); 359 | }); 360 | }); 361 | 362 | describe('extensions', () => { 363 | it('v0/amp-extension.js', () => { 364 | const input = 'https://cdn.ampproject.org/v0/amp-extension.js'; 365 | const expected = 366 | 'https://cdn.ampproject.org/rtv/RTV123/v0/amp-extension.js.map'; 367 | 368 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 369 | expect(actual).to.equal(expected); 370 | }); 371 | 372 | it('v0/amp-extension-module.js', () => { 373 | const input = 'https://cdn.ampproject.org/v0/amp-extension-module.js'; 374 | const expected = 375 | 'https://cdn.ampproject.org/rtv/RTV123/v0/amp-extension-module.js.map'; 376 | 377 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 378 | expect(actual).to.equal(expected); 379 | }); 380 | 381 | it('v0/amp-extension.js', () => { 382 | const input = 'https://cdn.ampproject.org/v0/amp-extension.js'; 383 | const expected = 384 | 'https://cdn.ampproject.org/rtv/RTV123/v0/amp-extension.js.map'; 385 | 386 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 387 | expect(actual).to.equal(expected); 388 | }); 389 | 390 | it('v1/amp-extension.js', () => { 391 | const input = 'https://cdn.ampproject.org/v1/amp-extension.js'; 392 | const expected = 393 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension.js.map'; 394 | 395 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 396 | expect(actual).to.equal(expected); 397 | }); 398 | 399 | it('v1/amp-extension-module.js', () => { 400 | const input = 'https://cdn.ampproject.org/v1/amp-extension-module.js'; 401 | const expected = 402 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension-module.js.map'; 403 | 404 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 405 | expect(actual).to.equal(expected); 406 | }); 407 | 408 | it('v1/amp-extension.js', () => { 409 | const input = 'https://cdn.ampproject.org/v1/amp-extension.js'; 410 | const expected = 411 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension.js.map'; 412 | 413 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 414 | expect(actual).to.equal(expected); 415 | }); 416 | 417 | it('v1/amp-extension.js', () => { 418 | const input = 'https://cdn.ampproject.org/v1/amp-extension.js'; 419 | const expected = 420 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension.js.map'; 421 | 422 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 423 | expect(actual).to.equal(expected); 424 | }); 425 | 426 | it('v1/amp-extension-module.js', () => { 427 | const input = 'https://cdn.ampproject.org/v1/amp-extension-module.js'; 428 | const expected = 429 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension-module.js.map'; 430 | 431 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 432 | expect(actual).to.equal(expected); 433 | }); 434 | 435 | it('v1/amp-extension.js', () => { 436 | const input = 'https://cdn.ampproject.org/v1/amp-extension.js'; 437 | const expected = 438 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension.js.map'; 439 | 440 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 441 | expect(actual).to.equal(expected); 442 | }); 443 | }); 444 | 445 | describe('rtvs', () => { 446 | it('rtv/010123456789123/v0.js', () => { 447 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v0.js'; 448 | const expected = 449 | 'https://cdn.ampproject.org/rtv/010123456789123/v0.js.map'; 450 | 451 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 452 | expect(actual).to.equal(expected); 453 | }); 454 | 455 | it('rtv/010123456789123/v0-module.js', () => { 456 | const input = 457 | 'https://cdn.ampproject.org/rtv/010123456789123/v0-module.js'; 458 | const expected = 459 | 'https://cdn.ampproject.org/rtv/010123456789123/v0-module.js.map'; 460 | 461 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 462 | expect(actual).to.equal(expected); 463 | }); 464 | 465 | it('rtv/010123456789123/v0.js', () => { 466 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v0.js'; 467 | const expected = 468 | 'https://cdn.ampproject.org/rtv/010123456789123/v0.js.map'; 469 | 470 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 471 | expect(actual).to.equal(expected); 472 | }); 473 | 474 | it('rtv/010123456789123/v1.js', () => { 475 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v1.js'; 476 | const expected = 477 | 'https://cdn.ampproject.org/rtv/010123456789123/v1.js.map'; 478 | 479 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 480 | expect(actual).to.equal(expected); 481 | }); 482 | 483 | it('rtv/010123456789123/v1-module.js', () => { 484 | const input = 485 | 'https://cdn.ampproject.org/rtv/010123456789123/v1-module.js'; 486 | const expected = 487 | 'https://cdn.ampproject.org/rtv/010123456789123/v1-module.js.map'; 488 | 489 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 490 | expect(actual).to.equal(expected); 491 | }); 492 | 493 | it('rtv/010123456789123/v1.js', () => { 494 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v1.js'; 495 | const expected = 496 | 'https://cdn.ampproject.org/rtv/010123456789123/v1.js.map'; 497 | 498 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 499 | expect(actual).to.equal(expected); 500 | }); 501 | 502 | it('rtv/010123456789123/v20.js', () => { 503 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v20.js'; 504 | const expected = 505 | 'https://cdn.ampproject.org/rtv/010123456789123/v20.js.map'; 506 | 507 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 508 | expect(actual).to.equal(expected); 509 | }); 510 | 511 | it('rtv/010123456789123/v20-module.js', () => { 512 | const input = 513 | 'https://cdn.ampproject.org/rtv/010123456789123/v20-module.js'; 514 | const expected = 515 | 'https://cdn.ampproject.org/rtv/010123456789123/v20-module.js.map'; 516 | 517 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 518 | expect(actual).to.equal(expected); 519 | }); 520 | 521 | it('rtv/010123456789123/v20.js', () => { 522 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v20.js'; 523 | const expected = 524 | 'https://cdn.ampproject.org/rtv/010123456789123/v20.js.map'; 525 | 526 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 527 | expect(actual).to.equal(expected); 528 | }); 529 | 530 | it('rtv/010123456789123/v0/amp-extension.js', () => { 531 | const input = 532 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.js'; 533 | const expected = 534 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.js.map'; 535 | 536 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 537 | expect(actual).to.equal(expected); 538 | }); 539 | 540 | it('rtv/010123456789123/v0/amp-extension-module.js', () => { 541 | const input = 542 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension-module.js'; 543 | const expected = 544 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension-module.js.map'; 545 | 546 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 547 | expect(actual).to.equal(expected); 548 | }); 549 | 550 | it('rtv/010123456789123/v0/amp-extension.js', () => { 551 | const input = 552 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.js'; 553 | const expected = 554 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.js.map'; 555 | 556 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 557 | expect(actual).to.equal(expected); 558 | }); 559 | 560 | it('rtv/010123456789123/v1/amp-extension.js', () => { 561 | const input = 562 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.js'; 563 | const expected = 564 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.js.map'; 565 | 566 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 567 | expect(actual).to.equal(expected); 568 | }); 569 | 570 | it('rtv/010123456789123/v1/amp-extension-module.js', () => { 571 | const input = 572 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension-module.js'; 573 | const expected = 574 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension-module.js.map'; 575 | 576 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 577 | expect(actual).to.equal(expected); 578 | }); 579 | 580 | it('rtv/010123456789123/v1/amp-extension.js', () => { 581 | const input = 582 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.js'; 583 | const expected = 584 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.js.map'; 585 | 586 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 587 | expect(actual).to.equal(expected); 588 | }); 589 | 590 | it('rtv/010123456789123/v20/amp-extension.js', () => { 591 | const input = 592 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.js'; 593 | const expected = 594 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.js.map'; 595 | 596 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 597 | expect(actual).to.equal(expected); 598 | }); 599 | 600 | it('rtv/010123456789123/v20/amp-extension-module.js', () => { 601 | const input = 602 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension-module.js'; 603 | const expected = 604 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension-module.js.map'; 605 | 606 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 607 | expect(actual).to.equal(expected); 608 | }); 609 | 610 | it('rtv/010123456789123/v20/amp-extension.js', () => { 611 | const input = 612 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.js'; 613 | const expected = 614 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.js.map'; 615 | 616 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 617 | expect(actual).to.equal(expected); 618 | }); 619 | }); 620 | 621 | describe('mjs', () => { 622 | it('v0.mjs', () => { 623 | const input = 'https://cdn.ampproject.org/v0.mjs'; 624 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v0.mjs.map'; 625 | 626 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 627 | expect(actual).to.equal(expected); 628 | }); 629 | 630 | it('v0-module.mjs', () => { 631 | const input = 'https://cdn.ampproject.org/v0-module.mjs'; 632 | const expected = 633 | 'https://cdn.ampproject.org/rtv/RTV123/v0-module.mjs.map'; 634 | 635 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 636 | expect(actual).to.equal(expected); 637 | }); 638 | 639 | it('v0.mjs', () => { 640 | const input = 'https://cdn.ampproject.org/v0.mjs'; 641 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v0.mjs.map'; 642 | 643 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 644 | expect(actual).to.equal(expected); 645 | }); 646 | 647 | it('v1.mjs', () => { 648 | const input = 'https://cdn.ampproject.org/v1.mjs'; 649 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v1.mjs.map'; 650 | 651 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 652 | expect(actual).to.equal(expected); 653 | }); 654 | 655 | it('v1-module.mjs', () => { 656 | const input = 'https://cdn.ampproject.org/v1-module.mjs'; 657 | const expected = 658 | 'https://cdn.ampproject.org/rtv/RTV123/v1-module.mjs.map'; 659 | 660 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 661 | expect(actual).to.equal(expected); 662 | }); 663 | 664 | it('v1.mjs', () => { 665 | const input = 'https://cdn.ampproject.org/v1.mjs'; 666 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v1.mjs.map'; 667 | 668 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 669 | expect(actual).to.equal(expected); 670 | }); 671 | 672 | it('v20.mjs', () => { 673 | const input = 'https://cdn.ampproject.org/v20.mjs'; 674 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v20.mjs.map'; 675 | 676 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 677 | expect(actual).to.equal(expected); 678 | }); 679 | 680 | it('v20-module.mjs', () => { 681 | const input = 'https://cdn.ampproject.org/v20-module.mjs'; 682 | const expected = 683 | 'https://cdn.ampproject.org/rtv/RTV123/v20-module.mjs.map'; 684 | 685 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 686 | expect(actual).to.equal(expected); 687 | }); 688 | 689 | it('v20.mjs', () => { 690 | const input = 'https://cdn.ampproject.org/v20.mjs'; 691 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v20.mjs.map'; 692 | 693 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 694 | expect(actual).to.equal(expected); 695 | }); 696 | 697 | it('v0/amp-extension.mjs', () => { 698 | const input = 'https://cdn.ampproject.org/v0/amp-extension.mjs'; 699 | const expected = 700 | 'https://cdn.ampproject.org/rtv/RTV123/v0/amp-extension.mjs.map'; 701 | 702 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 703 | expect(actual).to.equal(expected); 704 | }); 705 | 706 | it('v0/amp-extension-module.mjs', () => { 707 | const input = 'https://cdn.ampproject.org/v0/amp-extension-module.mjs'; 708 | const expected = 709 | 'https://cdn.ampproject.org/rtv/RTV123/v0/amp-extension-module.mjs.map'; 710 | 711 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 712 | expect(actual).to.equal(expected); 713 | }); 714 | 715 | it('v0/amp-extension.mjs', () => { 716 | const input = 'https://cdn.ampproject.org/v0/amp-extension.mjs'; 717 | const expected = 718 | 'https://cdn.ampproject.org/rtv/RTV123/v0/amp-extension.mjs.map'; 719 | 720 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 721 | expect(actual).to.equal(expected); 722 | }); 723 | 724 | it('v1/amp-extension.mjs', () => { 725 | const input = 'https://cdn.ampproject.org/v1/amp-extension.mjs'; 726 | const expected = 727 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension.mjs.map'; 728 | 729 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 730 | expect(actual).to.equal(expected); 731 | }); 732 | 733 | it('v1/amp-extension-module.mjs', () => { 734 | const input = 'https://cdn.ampproject.org/v1/amp-extension-module.mjs'; 735 | const expected = 736 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension-module.mjs.map'; 737 | 738 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 739 | expect(actual).to.equal(expected); 740 | }); 741 | 742 | it('v1/amp-extension.mjs', () => { 743 | const input = 'https://cdn.ampproject.org/v1/amp-extension.mjs'; 744 | const expected = 745 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension.mjs.map'; 746 | 747 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 748 | expect(actual).to.equal(expected); 749 | }); 750 | 751 | it('v20/amp-extension.mjs', () => { 752 | const input = 'https://cdn.ampproject.org/v20/amp-extension.mjs'; 753 | const expected = 754 | 'https://cdn.ampproject.org/rtv/RTV123/v20/amp-extension.mjs.map'; 755 | 756 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 757 | expect(actual).to.equal(expected); 758 | }); 759 | 760 | it('v20/amp-extension-module.mjs', () => { 761 | const input = 'https://cdn.ampproject.org/v20/amp-extension-module.mjs'; 762 | const expected = 763 | 'https://cdn.ampproject.org/rtv/RTV123/v20/amp-extension-module.mjs.map'; 764 | 765 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 766 | expect(actual).to.equal(expected); 767 | }); 768 | 769 | it('v20/amp-extension.mjs', () => { 770 | const input = 'https://cdn.ampproject.org/v20/amp-extension.mjs'; 771 | const expected = 772 | 'https://cdn.ampproject.org/rtv/RTV123/v20/amp-extension.mjs.map'; 773 | 774 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 775 | expect(actual).to.equal(expected); 776 | }); 777 | 778 | it('rtv/010123456789123/v0.mjs', () => { 779 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v0.mjs'; 780 | const expected = 781 | 'https://cdn.ampproject.org/rtv/010123456789123/v0.mjs.map'; 782 | 783 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 784 | expect(actual).to.equal(expected); 785 | }); 786 | 787 | it('rtv/010123456789123/v0-module.mjs', () => { 788 | const input = 789 | 'https://cdn.ampproject.org/rtv/010123456789123/v0-module.mjs'; 790 | const expected = 791 | 'https://cdn.ampproject.org/rtv/010123456789123/v0-module.mjs.map'; 792 | 793 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 794 | expect(actual).to.equal(expected); 795 | }); 796 | 797 | it('rtv/010123456789123/v0.mjs', () => { 798 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v0.mjs'; 799 | const expected = 800 | 'https://cdn.ampproject.org/rtv/010123456789123/v0.mjs.map'; 801 | 802 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 803 | expect(actual).to.equal(expected); 804 | }); 805 | 806 | it('rtv/010123456789123/v1.mjs', () => { 807 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v1.mjs'; 808 | const expected = 809 | 'https://cdn.ampproject.org/rtv/010123456789123/v1.mjs.map'; 810 | 811 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 812 | expect(actual).to.equal(expected); 813 | }); 814 | 815 | it('rtv/010123456789123/v1-module.mjs', () => { 816 | const input = 817 | 'https://cdn.ampproject.org/rtv/010123456789123/v1-module.mjs'; 818 | const expected = 819 | 'https://cdn.ampproject.org/rtv/010123456789123/v1-module.mjs.map'; 820 | 821 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 822 | expect(actual).to.equal(expected); 823 | }); 824 | 825 | it('rtv/010123456789123/v1.mjs', () => { 826 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v1.mjs'; 827 | const expected = 828 | 'https://cdn.ampproject.org/rtv/010123456789123/v1.mjs.map'; 829 | 830 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 831 | expect(actual).to.equal(expected); 832 | }); 833 | 834 | it('rtv/010123456789123/v20.mjs', () => { 835 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v20.mjs'; 836 | const expected = 837 | 'https://cdn.ampproject.org/rtv/010123456789123/v20.mjs.map'; 838 | 839 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 840 | expect(actual).to.equal(expected); 841 | }); 842 | 843 | it('rtv/010123456789123/v20-module.mjs', () => { 844 | const input = 845 | 'https://cdn.ampproject.org/rtv/010123456789123/v20-module.mjs'; 846 | const expected = 847 | 'https://cdn.ampproject.org/rtv/010123456789123/v20-module.mjs.map'; 848 | 849 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 850 | expect(actual).to.equal(expected); 851 | }); 852 | 853 | it('rtv/010123456789123/v20.mjs', () => { 854 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v20.mjs'; 855 | const expected = 856 | 'https://cdn.ampproject.org/rtv/010123456789123/v20.mjs.map'; 857 | 858 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 859 | expect(actual).to.equal(expected); 860 | }); 861 | 862 | it('rtv/010123456789123/v0/amp-extension.mjs', () => { 863 | const input = 864 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.mjs'; 865 | const expected = 866 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.mjs.map'; 867 | 868 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 869 | expect(actual).to.equal(expected); 870 | }); 871 | 872 | it('rtv/010123456789123/v0/amp-extension-module.mjs', () => { 873 | const input = 874 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension-module.mjs'; 875 | const expected = 876 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension-module.mjs.map'; 877 | 878 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 879 | expect(actual).to.equal(expected); 880 | }); 881 | 882 | it('rtv/010123456789123/v0/amp-extension.mjs', () => { 883 | const input = 884 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.mjs'; 885 | const expected = 886 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.mjs.map'; 887 | 888 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 889 | expect(actual).to.equal(expected); 890 | }); 891 | 892 | it('rtv/010123456789123/v1/amp-extension.mjs', () => { 893 | const input = 894 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.mjs'; 895 | const expected = 896 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.mjs.map'; 897 | 898 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 899 | expect(actual).to.equal(expected); 900 | }); 901 | 902 | it('rtv/010123456789123/v1/amp-extension-module.mjs', () => { 903 | const input = 904 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension-module.mjs'; 905 | const expected = 906 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension-module.mjs.map'; 907 | 908 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 909 | expect(actual).to.equal(expected); 910 | }); 911 | 912 | it('rtv/010123456789123/v1/amp-extension.mjs', () => { 913 | const input = 914 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.mjs'; 915 | const expected = 916 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.mjs.map'; 917 | 918 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 919 | expect(actual).to.equal(expected); 920 | }); 921 | 922 | it('rtv/010123456789123/v20/amp-extension.mjs', () => { 923 | const input = 924 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.mjs'; 925 | const expected = 926 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.mjs.map'; 927 | 928 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 929 | expect(actual).to.equal(expected); 930 | }); 931 | 932 | it('rtv/010123456789123/v20/amp-extension-module.mjs', () => { 933 | const input = 934 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension-module.mjs'; 935 | const expected = 936 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension-module.mjs.map'; 937 | 938 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 939 | expect(actual).to.equal(expected); 940 | }); 941 | 942 | it('rtv/010123456789123/v20/amp-extension.mjs', () => { 943 | const input = 944 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.mjs'; 945 | const expected = 946 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.mjs.map'; 947 | 948 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 949 | expect(actual).to.equal(expected); 950 | }); 951 | }); 952 | 953 | describe('brotli', () => { 954 | it('v0.js.br', () => { 955 | const input = 'https://cdn.ampproject.org/v0.js.br'; 956 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v0.js.map'; 957 | 958 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 959 | expect(actual).to.equal(expected); 960 | }); 961 | 962 | it('v0-module.js.br', () => { 963 | const input = 'https://cdn.ampproject.org/v0-module.js.br'; 964 | const expected = 965 | 'https://cdn.ampproject.org/rtv/RTV123/v0-module.js.map'; 966 | 967 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 968 | expect(actual).to.equal(expected); 969 | }); 970 | 971 | it('v0.js.br', () => { 972 | const input = 'https://cdn.ampproject.org/v0.js.br'; 973 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v0.js.map'; 974 | 975 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 976 | expect(actual).to.equal(expected); 977 | }); 978 | 979 | it('v1.js.br', () => { 980 | const input = 'https://cdn.ampproject.org/v1.js.br'; 981 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v1.js.map'; 982 | 983 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 984 | expect(actual).to.equal(expected); 985 | }); 986 | 987 | it('v1-module.js.br', () => { 988 | const input = 'https://cdn.ampproject.org/v1-module.js.br'; 989 | const expected = 990 | 'https://cdn.ampproject.org/rtv/RTV123/v1-module.js.map'; 991 | 992 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 993 | expect(actual).to.equal(expected); 994 | }); 995 | 996 | it('v1.js.br', () => { 997 | const input = 'https://cdn.ampproject.org/v1.js.br'; 998 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v1.js.map'; 999 | 1000 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1001 | expect(actual).to.equal(expected); 1002 | }); 1003 | 1004 | it('v20.js.br', () => { 1005 | const input = 'https://cdn.ampproject.org/v20.js.br'; 1006 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v20.js.map'; 1007 | 1008 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1009 | expect(actual).to.equal(expected); 1010 | }); 1011 | 1012 | it('v20-module.js.br', () => { 1013 | const input = 'https://cdn.ampproject.org/v20-module.js.br'; 1014 | const expected = 1015 | 'https://cdn.ampproject.org/rtv/RTV123/v20-module.js.map'; 1016 | 1017 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1018 | expect(actual).to.equal(expected); 1019 | }); 1020 | 1021 | it('v20.js.br', () => { 1022 | const input = 'https://cdn.ampproject.org/v20.js.br'; 1023 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v20.js.map'; 1024 | 1025 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1026 | expect(actual).to.equal(expected); 1027 | }); 1028 | 1029 | it('v0/amp-extension.js.br', () => { 1030 | const input = 'https://cdn.ampproject.org/v0/amp-extension.js.br'; 1031 | const expected = 1032 | 'https://cdn.ampproject.org/rtv/RTV123/v0/amp-extension.js.map'; 1033 | 1034 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1035 | expect(actual).to.equal(expected); 1036 | }); 1037 | 1038 | it('v0/amp-extension-module.js.br', () => { 1039 | const input = 1040 | 'https://cdn.ampproject.org/v0/amp-extension-module.js.br'; 1041 | const expected = 1042 | 'https://cdn.ampproject.org/rtv/RTV123/v0/amp-extension-module.js.map'; 1043 | 1044 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1045 | expect(actual).to.equal(expected); 1046 | }); 1047 | 1048 | it('v0/amp-extension.js.br', () => { 1049 | const input = 'https://cdn.ampproject.org/v0/amp-extension.js.br'; 1050 | const expected = 1051 | 'https://cdn.ampproject.org/rtv/RTV123/v0/amp-extension.js.map'; 1052 | 1053 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1054 | expect(actual).to.equal(expected); 1055 | }); 1056 | 1057 | it('v1/amp-extension.js.br', () => { 1058 | const input = 'https://cdn.ampproject.org/v1/amp-extension.js.br'; 1059 | const expected = 1060 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension.js.map'; 1061 | 1062 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1063 | expect(actual).to.equal(expected); 1064 | }); 1065 | 1066 | it('v1/amp-extension-module.js.br', () => { 1067 | const input = 1068 | 'https://cdn.ampproject.org/v1/amp-extension-module.js.br'; 1069 | const expected = 1070 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension-module.js.map'; 1071 | 1072 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1073 | expect(actual).to.equal(expected); 1074 | }); 1075 | 1076 | it('v1/amp-extension.js.br', () => { 1077 | const input = 'https://cdn.ampproject.org/v1/amp-extension.js.br'; 1078 | const expected = 1079 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension.js.map'; 1080 | 1081 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1082 | expect(actual).to.equal(expected); 1083 | }); 1084 | 1085 | it('v20/amp-extension.js.br', () => { 1086 | const input = 'https://cdn.ampproject.org/v20/amp-extension.js.br'; 1087 | const expected = 1088 | 'https://cdn.ampproject.org/rtv/RTV123/v20/amp-extension.js.map'; 1089 | 1090 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1091 | expect(actual).to.equal(expected); 1092 | }); 1093 | 1094 | it('v20/amp-extension-module.js.br', () => { 1095 | const input = 1096 | 'https://cdn.ampproject.org/v20/amp-extension-module.js.br'; 1097 | const expected = 1098 | 'https://cdn.ampproject.org/rtv/RTV123/v20/amp-extension-module.js.map'; 1099 | 1100 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1101 | expect(actual).to.equal(expected); 1102 | }); 1103 | 1104 | it('v20/amp-extension.js.br', () => { 1105 | const input = 'https://cdn.ampproject.org/v20/amp-extension.js.br'; 1106 | const expected = 1107 | 'https://cdn.ampproject.org/rtv/RTV123/v20/amp-extension.js.map'; 1108 | 1109 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1110 | expect(actual).to.equal(expected); 1111 | }); 1112 | 1113 | it('rtv/010123456789123/v0.js.br', () => { 1114 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v0.js.br'; 1115 | const expected = 1116 | 'https://cdn.ampproject.org/rtv/010123456789123/v0.js.map'; 1117 | 1118 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1119 | expect(actual).to.equal(expected); 1120 | }); 1121 | 1122 | it('rtv/010123456789123/v0-module.js.br', () => { 1123 | const input = 1124 | 'https://cdn.ampproject.org/rtv/010123456789123/v0-module.js.br'; 1125 | const expected = 1126 | 'https://cdn.ampproject.org/rtv/010123456789123/v0-module.js.map'; 1127 | 1128 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1129 | expect(actual).to.equal(expected); 1130 | }); 1131 | 1132 | it('rtv/010123456789123/v0.js.br', () => { 1133 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v0.js.br'; 1134 | const expected = 1135 | 'https://cdn.ampproject.org/rtv/010123456789123/v0.js.map'; 1136 | 1137 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1138 | expect(actual).to.equal(expected); 1139 | }); 1140 | 1141 | it('rtv/010123456789123/v1.js.br', () => { 1142 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v1.js.br'; 1143 | const expected = 1144 | 'https://cdn.ampproject.org/rtv/010123456789123/v1.js.map'; 1145 | 1146 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1147 | expect(actual).to.equal(expected); 1148 | }); 1149 | 1150 | it('rtv/010123456789123/v1-module.js.br', () => { 1151 | const input = 1152 | 'https://cdn.ampproject.org/rtv/010123456789123/v1-module.js.br'; 1153 | const expected = 1154 | 'https://cdn.ampproject.org/rtv/010123456789123/v1-module.js.map'; 1155 | 1156 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1157 | expect(actual).to.equal(expected); 1158 | }); 1159 | 1160 | it('rtv/010123456789123/v1.js.br', () => { 1161 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v1.js.br'; 1162 | const expected = 1163 | 'https://cdn.ampproject.org/rtv/010123456789123/v1.js.map'; 1164 | 1165 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1166 | expect(actual).to.equal(expected); 1167 | }); 1168 | 1169 | it('rtv/010123456789123/v20.js.br', () => { 1170 | const input = 1171 | 'https://cdn.ampproject.org/rtv/010123456789123/v20.js.br'; 1172 | const expected = 1173 | 'https://cdn.ampproject.org/rtv/010123456789123/v20.js.map'; 1174 | 1175 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1176 | expect(actual).to.equal(expected); 1177 | }); 1178 | 1179 | it('rtv/010123456789123/v20-module.js.br', () => { 1180 | const input = 1181 | 'https://cdn.ampproject.org/rtv/010123456789123/v20-module.js.br'; 1182 | const expected = 1183 | 'https://cdn.ampproject.org/rtv/010123456789123/v20-module.js.map'; 1184 | 1185 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1186 | expect(actual).to.equal(expected); 1187 | }); 1188 | 1189 | it('rtv/010123456789123/v20.js.br', () => { 1190 | const input = 1191 | 'https://cdn.ampproject.org/rtv/010123456789123/v20.js.br'; 1192 | const expected = 1193 | 'https://cdn.ampproject.org/rtv/010123456789123/v20.js.map'; 1194 | 1195 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1196 | expect(actual).to.equal(expected); 1197 | }); 1198 | 1199 | it('rtv/010123456789123/v0/amp-extension.js.br', () => { 1200 | const input = 1201 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.js.br'; 1202 | const expected = 1203 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.js.map'; 1204 | 1205 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1206 | expect(actual).to.equal(expected); 1207 | }); 1208 | 1209 | it('rtv/010123456789123/v0/amp-extension-module.js.br', () => { 1210 | const input = 1211 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension-module.js.br'; 1212 | const expected = 1213 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension-module.js.map'; 1214 | 1215 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1216 | expect(actual).to.equal(expected); 1217 | }); 1218 | 1219 | it('rtv/010123456789123/v0/amp-extension.js.br', () => { 1220 | const input = 1221 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.js.br'; 1222 | const expected = 1223 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.js.map'; 1224 | 1225 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1226 | expect(actual).to.equal(expected); 1227 | }); 1228 | 1229 | it('rtv/010123456789123/v1/amp-extension.js.br', () => { 1230 | const input = 1231 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.js.br'; 1232 | const expected = 1233 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.js.map'; 1234 | 1235 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1236 | expect(actual).to.equal(expected); 1237 | }); 1238 | 1239 | it('rtv/010123456789123/v1/amp-extension-module.js.br', () => { 1240 | const input = 1241 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension-module.js.br'; 1242 | const expected = 1243 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension-module.js.map'; 1244 | 1245 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1246 | expect(actual).to.equal(expected); 1247 | }); 1248 | 1249 | it('rtv/010123456789123/v1/amp-extension.js.br', () => { 1250 | const input = 1251 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.js.br'; 1252 | const expected = 1253 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.js.map'; 1254 | 1255 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1256 | expect(actual).to.equal(expected); 1257 | }); 1258 | 1259 | it('rtv/010123456789123/v20/amp-extension.js.br', () => { 1260 | const input = 1261 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.js.br'; 1262 | const expected = 1263 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.js.map'; 1264 | 1265 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1266 | expect(actual).to.equal(expected); 1267 | }); 1268 | 1269 | it('rtv/010123456789123/v20/amp-extension-module.js.br', () => { 1270 | const input = 1271 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension-module.js.br'; 1272 | const expected = 1273 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension-module.js.map'; 1274 | 1275 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1276 | expect(actual).to.equal(expected); 1277 | }); 1278 | 1279 | it('rtv/010123456789123/v20/amp-extension.js.br', () => { 1280 | const input = 1281 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.js.br'; 1282 | const expected = 1283 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.js.map'; 1284 | 1285 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1286 | expect(actual).to.equal(expected); 1287 | }); 1288 | 1289 | it('v0.js.br', () => { 1290 | const input = 'https://cdn.ampproject.org/v0.js.br'; 1291 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v0.js.map'; 1292 | 1293 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1294 | expect(actual).to.equal(expected); 1295 | }); 1296 | 1297 | it('v0-module.js.br', () => { 1298 | const input = 'https://cdn.ampproject.org/v0-module.js.br'; 1299 | const expected = 1300 | 'https://cdn.ampproject.org/rtv/RTV123/v0-module.js.map'; 1301 | 1302 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1303 | expect(actual).to.equal(expected); 1304 | }); 1305 | 1306 | it('v0.js.br', () => { 1307 | const input = 'https://cdn.ampproject.org/v0.js.br'; 1308 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v0.js.map'; 1309 | 1310 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1311 | expect(actual).to.equal(expected); 1312 | }); 1313 | 1314 | it('v1.js.br', () => { 1315 | const input = 'https://cdn.ampproject.org/v1.js.br'; 1316 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v1.js.map'; 1317 | 1318 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1319 | expect(actual).to.equal(expected); 1320 | }); 1321 | 1322 | it('v1-module.js.br', () => { 1323 | const input = 'https://cdn.ampproject.org/v1-module.js.br'; 1324 | const expected = 1325 | 'https://cdn.ampproject.org/rtv/RTV123/v1-module.js.map'; 1326 | 1327 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1328 | expect(actual).to.equal(expected); 1329 | }); 1330 | 1331 | it('v1.js.br', () => { 1332 | const input = 'https://cdn.ampproject.org/v1.js.br'; 1333 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v1.js.map'; 1334 | 1335 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1336 | expect(actual).to.equal(expected); 1337 | }); 1338 | 1339 | it('v20.js.br', () => { 1340 | const input = 'https://cdn.ampproject.org/v20.js.br'; 1341 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v20.js.map'; 1342 | 1343 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1344 | expect(actual).to.equal(expected); 1345 | }); 1346 | 1347 | it('v20-module.js.br', () => { 1348 | const input = 'https://cdn.ampproject.org/v20-module.js.br'; 1349 | const expected = 1350 | 'https://cdn.ampproject.org/rtv/RTV123/v20-module.js.map'; 1351 | 1352 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1353 | expect(actual).to.equal(expected); 1354 | }); 1355 | 1356 | it('v20.js.br', () => { 1357 | const input = 'https://cdn.ampproject.org/v20.js.br'; 1358 | const expected = 'https://cdn.ampproject.org/rtv/RTV123/v20.js.map'; 1359 | 1360 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1361 | expect(actual).to.equal(expected); 1362 | }); 1363 | 1364 | it('v0/amp-extension.js.br', () => { 1365 | const input = 'https://cdn.ampproject.org/v0/amp-extension.js.br'; 1366 | const expected = 1367 | 'https://cdn.ampproject.org/rtv/RTV123/v0/amp-extension.js.map'; 1368 | 1369 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1370 | expect(actual).to.equal(expected); 1371 | }); 1372 | 1373 | it('v0/amp-extension-module.js.br', () => { 1374 | const input = 1375 | 'https://cdn.ampproject.org/v0/amp-extension-module.js.br'; 1376 | const expected = 1377 | 'https://cdn.ampproject.org/rtv/RTV123/v0/amp-extension-module.js.map'; 1378 | 1379 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1380 | expect(actual).to.equal(expected); 1381 | }); 1382 | 1383 | it('v0/amp-extension.js.br', () => { 1384 | const input = 'https://cdn.ampproject.org/v0/amp-extension.js.br'; 1385 | const expected = 1386 | 'https://cdn.ampproject.org/rtv/RTV123/v0/amp-extension.js.map'; 1387 | 1388 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1389 | expect(actual).to.equal(expected); 1390 | }); 1391 | 1392 | it('v1/amp-extension.js.br', () => { 1393 | const input = 'https://cdn.ampproject.org/v1/amp-extension.js.br'; 1394 | const expected = 1395 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension.js.map'; 1396 | 1397 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1398 | expect(actual).to.equal(expected); 1399 | }); 1400 | 1401 | it('v1/amp-extension-module.js.br', () => { 1402 | const input = 1403 | 'https://cdn.ampproject.org/v1/amp-extension-module.js.br'; 1404 | const expected = 1405 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension-module.js.map'; 1406 | 1407 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1408 | expect(actual).to.equal(expected); 1409 | }); 1410 | 1411 | it('v1/amp-extension.js.br', () => { 1412 | const input = 'https://cdn.ampproject.org/v1/amp-extension.js.br'; 1413 | const expected = 1414 | 'https://cdn.ampproject.org/rtv/RTV123/v1/amp-extension.js.map'; 1415 | 1416 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1417 | expect(actual).to.equal(expected); 1418 | }); 1419 | 1420 | it('v20/amp-extension.js.br', () => { 1421 | const input = 'https://cdn.ampproject.org/v20/amp-extension.js.br'; 1422 | const expected = 1423 | 'https://cdn.ampproject.org/rtv/RTV123/v20/amp-extension.js.map'; 1424 | 1425 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1426 | expect(actual).to.equal(expected); 1427 | }); 1428 | 1429 | it('v20/amp-extension-module.js.br', () => { 1430 | const input = 1431 | 'https://cdn.ampproject.org/v20/amp-extension-module.js.br'; 1432 | const expected = 1433 | 'https://cdn.ampproject.org/rtv/RTV123/v20/amp-extension-module.js.map'; 1434 | 1435 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1436 | expect(actual).to.equal(expected); 1437 | }); 1438 | 1439 | it('v20/amp-extension.js.br', () => { 1440 | const input = 'https://cdn.ampproject.org/v20/amp-extension.js.br'; 1441 | const expected = 1442 | 'https://cdn.ampproject.org/rtv/RTV123/v20/amp-extension.js.map'; 1443 | 1444 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1445 | expect(actual).to.equal(expected); 1446 | }); 1447 | 1448 | it('rtv/010123456789123/v0.js.br', () => { 1449 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v0.js.br'; 1450 | const expected = 1451 | 'https://cdn.ampproject.org/rtv/010123456789123/v0.js.map'; 1452 | 1453 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1454 | expect(actual).to.equal(expected); 1455 | }); 1456 | 1457 | it('rtv/010123456789123/v0-module.js.br', () => { 1458 | const input = 1459 | 'https://cdn.ampproject.org/rtv/010123456789123/v0-module.js.br'; 1460 | const expected = 1461 | 'https://cdn.ampproject.org/rtv/010123456789123/v0-module.js.map'; 1462 | 1463 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1464 | expect(actual).to.equal(expected); 1465 | }); 1466 | 1467 | it('rtv/010123456789123/v0.js.br', () => { 1468 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v0.js.br'; 1469 | const expected = 1470 | 'https://cdn.ampproject.org/rtv/010123456789123/v0.js.map'; 1471 | 1472 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1473 | expect(actual).to.equal(expected); 1474 | }); 1475 | 1476 | it('rtv/010123456789123/v1.js.br', () => { 1477 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v1.js.br'; 1478 | const expected = 1479 | 'https://cdn.ampproject.org/rtv/010123456789123/v1.js.map'; 1480 | 1481 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1482 | expect(actual).to.equal(expected); 1483 | }); 1484 | 1485 | it('rtv/010123456789123/v1-module.js.br', () => { 1486 | const input = 1487 | 'https://cdn.ampproject.org/rtv/010123456789123/v1-module.js.br'; 1488 | const expected = 1489 | 'https://cdn.ampproject.org/rtv/010123456789123/v1-module.js.map'; 1490 | 1491 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1492 | expect(actual).to.equal(expected); 1493 | }); 1494 | 1495 | it('rtv/010123456789123/v1.js.br', () => { 1496 | const input = 'https://cdn.ampproject.org/rtv/010123456789123/v1.js.br'; 1497 | const expected = 1498 | 'https://cdn.ampproject.org/rtv/010123456789123/v1.js.map'; 1499 | 1500 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1501 | expect(actual).to.equal(expected); 1502 | }); 1503 | 1504 | it('rtv/010123456789123/v20.js.br', () => { 1505 | const input = 1506 | 'https://cdn.ampproject.org/rtv/010123456789123/v20.js.br'; 1507 | const expected = 1508 | 'https://cdn.ampproject.org/rtv/010123456789123/v20.js.map'; 1509 | 1510 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1511 | expect(actual).to.equal(expected); 1512 | }); 1513 | 1514 | it('rtv/010123456789123/v20-module.js.br', () => { 1515 | const input = 1516 | 'https://cdn.ampproject.org/rtv/010123456789123/v20-module.js.br'; 1517 | const expected = 1518 | 'https://cdn.ampproject.org/rtv/010123456789123/v20-module.js.map'; 1519 | 1520 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1521 | expect(actual).to.equal(expected); 1522 | }); 1523 | 1524 | it('rtv/010123456789123/v20.js.br', () => { 1525 | const input = 1526 | 'https://cdn.ampproject.org/rtv/010123456789123/v20.js.br'; 1527 | const expected = 1528 | 'https://cdn.ampproject.org/rtv/010123456789123/v20.js.map'; 1529 | 1530 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1531 | expect(actual).to.equal(expected); 1532 | }); 1533 | 1534 | it('rtv/010123456789123/v0/amp-extension.js.br', () => { 1535 | const input = 1536 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.js.br'; 1537 | const expected = 1538 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.js.map'; 1539 | 1540 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1541 | expect(actual).to.equal(expected); 1542 | }); 1543 | 1544 | it('rtv/010123456789123/v0/amp-extension-module.js.br', () => { 1545 | const input = 1546 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension-module.js.br'; 1547 | const expected = 1548 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension-module.js.map'; 1549 | 1550 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1551 | expect(actual).to.equal(expected); 1552 | }); 1553 | 1554 | it('rtv/010123456789123/v0/amp-extension.js.br', () => { 1555 | const input = 1556 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.js.br'; 1557 | const expected = 1558 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/amp-extension.js.map'; 1559 | 1560 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1561 | expect(actual).to.equal(expected); 1562 | }); 1563 | 1564 | it('rtv/010123456789123/v1/amp-extension.js.br', () => { 1565 | const input = 1566 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.js.br'; 1567 | const expected = 1568 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.js.map'; 1569 | 1570 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1571 | expect(actual).to.equal(expected); 1572 | }); 1573 | 1574 | it('rtv/010123456789123/v1/amp-extension-module.js.br', () => { 1575 | const input = 1576 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension-module.js.br'; 1577 | const expected = 1578 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension-module.js.map'; 1579 | 1580 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1581 | expect(actual).to.equal(expected); 1582 | }); 1583 | 1584 | it('rtv/010123456789123/v1/amp-extension.js.br', () => { 1585 | const input = 1586 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.js.br'; 1587 | const expected = 1588 | 'https://cdn.ampproject.org/rtv/010123456789123/v1/amp-extension.js.map'; 1589 | 1590 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1591 | expect(actual).to.equal(expected); 1592 | }); 1593 | 1594 | it('rtv/010123456789123/v20/amp-extension.js.br', () => { 1595 | const input = 1596 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.js.br'; 1597 | const expected = 1598 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.js.map'; 1599 | 1600 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1601 | expect(actual).to.equal(expected); 1602 | }); 1603 | 1604 | it('rtv/010123456789123/v20/amp-extension-module.js.br', () => { 1605 | const input = 1606 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension-module.js.br'; 1607 | const expected = 1608 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension-module.js.map'; 1609 | 1610 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1611 | expect(actual).to.equal(expected); 1612 | }); 1613 | 1614 | it('rtv/010123456789123/v20/amp-extension.js.br', () => { 1615 | const input = 1616 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.js.br'; 1617 | const expected = 1618 | 'https://cdn.ampproject.org/rtv/010123456789123/v20/amp-extension.js.map'; 1619 | 1620 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1621 | expect(actual).to.equal(expected); 1622 | }); 1623 | }); 1624 | 1625 | describe('validator js', () => { 1626 | it('v0/validator.js', () => { 1627 | const input = 'https://cdn.ampproject.org/v0/validator.js'; 1628 | const expected = ''; 1629 | 1630 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1631 | expect(actual).to.equal(expected); 1632 | }); 1633 | 1634 | it('v0/validator-module.js', () => { 1635 | const input = 'https://cdn.ampproject.org/v0/validator-module.js'; 1636 | const expected = ''; 1637 | 1638 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1639 | expect(actual).to.equal(expected); 1640 | }); 1641 | 1642 | it('v0/validator.js', () => { 1643 | const input = 'https://cdn.ampproject.org/v0/validator.js'; 1644 | const expected = ''; 1645 | 1646 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1647 | expect(actual).to.equal(expected); 1648 | }); 1649 | 1650 | it('v0/validator.mjs', () => { 1651 | const input = 'https://cdn.ampproject.org/v0/validator.mjs'; 1652 | const expected = ''; 1653 | 1654 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1655 | expect(actual).to.equal(expected); 1656 | }); 1657 | 1658 | it('v0/validator-module.mjs', () => { 1659 | const input = 'https://cdn.ampproject.org/v0/validator-module.mjs'; 1660 | const expected = ''; 1661 | 1662 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1663 | expect(actual).to.equal(expected); 1664 | }); 1665 | 1666 | it('v0/validator.mjs', () => { 1667 | const input = 'https://cdn.ampproject.org/v0/validator.mjs'; 1668 | const expected = ''; 1669 | 1670 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1671 | expect(actual).to.equal(expected); 1672 | }); 1673 | 1674 | it('rtv/010123456789123/v0/validator.js', () => { 1675 | const input = 1676 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/validator.js'; 1677 | const expected = ''; 1678 | 1679 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1680 | expect(actual).to.equal(expected); 1681 | }); 1682 | 1683 | it('rtv/010123456789123/v0/validator-module.js', () => { 1684 | const input = 1685 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/validator-module.js'; 1686 | const expected = ''; 1687 | 1688 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1689 | expect(actual).to.equal(expected); 1690 | }); 1691 | 1692 | it('rtv/010123456789123/v0/validator.js', () => { 1693 | const input = 1694 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/validator.js'; 1695 | const expected = ''; 1696 | 1697 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1698 | expect(actual).to.equal(expected); 1699 | }); 1700 | 1701 | it('rtv/010123456789123/v0/validator.mjs', () => { 1702 | const input = 1703 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/validator.mjs'; 1704 | const expected = ''; 1705 | 1706 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1707 | expect(actual).to.equal(expected); 1708 | }); 1709 | 1710 | it('rtv/010123456789123/v0/validator-module.mjs', () => { 1711 | const input = 1712 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/validator-module.mjs'; 1713 | const expected = ''; 1714 | 1715 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1716 | expect(actual).to.equal(expected); 1717 | }); 1718 | 1719 | it('rtv/010123456789123/v0/validator.mjs', () => { 1720 | const input = 1721 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/validator.mjs'; 1722 | const expected = ''; 1723 | 1724 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1725 | expect(actual).to.equal(expected); 1726 | }); 1727 | }); 1728 | 1729 | describe('experiments js', () => { 1730 | it('v0/experiments.js', () => { 1731 | const input = 'https://cdn.ampproject.org/v0/experiments.js'; 1732 | const expected = ''; 1733 | 1734 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1735 | expect(actual).to.equal(expected); 1736 | }); 1737 | 1738 | it('v0/experiments-module.js', () => { 1739 | const input = 'https://cdn.ampproject.org/v0/experiments-module.js'; 1740 | const expected = ''; 1741 | 1742 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1743 | expect(actual).to.equal(expected); 1744 | }); 1745 | 1746 | it('v0/experiments.js', () => { 1747 | const input = 'https://cdn.ampproject.org/v0/experiments.js'; 1748 | const expected = ''; 1749 | 1750 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1751 | expect(actual).to.equal(expected); 1752 | }); 1753 | 1754 | it('v0/experiments.mjs', () => { 1755 | const input = 'https://cdn.ampproject.org/v0/experiments.mjs'; 1756 | const expected = ''; 1757 | 1758 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1759 | expect(actual).to.equal(expected); 1760 | }); 1761 | 1762 | it('v0/experiments-module.mjs', () => { 1763 | const input = 'https://cdn.ampproject.org/v0/experiments-module.mjs'; 1764 | const expected = ''; 1765 | 1766 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1767 | expect(actual).to.equal(expected); 1768 | }); 1769 | 1770 | it('v0/experiments.mjs', () => { 1771 | const input = 'https://cdn.ampproject.org/v0/experiments.mjs'; 1772 | const expected = ''; 1773 | 1774 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1775 | expect(actual).to.equal(expected); 1776 | }); 1777 | 1778 | it('rtv/010123456789123/v0/experiments.js', () => { 1779 | const input = 1780 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/experiments.js'; 1781 | const expected = ''; 1782 | 1783 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1784 | expect(actual).to.equal(expected); 1785 | }); 1786 | 1787 | it('rtv/010123456789123/v0/experiments-module.js', () => { 1788 | const input = 1789 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/experiments-module.js'; 1790 | const expected = ''; 1791 | 1792 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1793 | expect(actual).to.equal(expected); 1794 | }); 1795 | 1796 | it('rtv/010123456789123/v0/experiments.js', () => { 1797 | const input = 1798 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/experiments.js'; 1799 | const expected = ''; 1800 | 1801 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1802 | expect(actual).to.equal(expected); 1803 | }); 1804 | 1805 | it('rtv/010123456789123/v0/experiments.mjs', () => { 1806 | const input = 1807 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/experiments.mjs'; 1808 | const expected = ''; 1809 | 1810 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1811 | expect(actual).to.equal(expected); 1812 | }); 1813 | 1814 | it('rtv/010123456789123/v0/experiments-module.mjs', () => { 1815 | const input = 1816 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/experiments-module.mjs'; 1817 | const expected = ''; 1818 | 1819 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1820 | expect(actual).to.equal(expected); 1821 | }); 1822 | 1823 | it('rtv/010123456789123/v0/experiments.mjs', () => { 1824 | const input = 1825 | 'https://cdn.ampproject.org/rtv/010123456789123/v0/experiments.mjs'; 1826 | const expected = ''; 1827 | 1828 | const actual = normalizeCdnJsUrl(input, 'RTV123'); 1829 | expect(actual).to.equal(expected); 1830 | }); 1831 | }); 1832 | }); 1833 | }); 1834 | -------------------------------------------------------------------------------- /utils/cache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS-IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import debounce from 'lodash.debounce'; 16 | 17 | /** 18 | * A wrapper around JS Map object to ensure no entry stays in map 19 | * more than 2 weeks without retrieval 20 | * @template T 21 | */ 22 | export class Cache { 23 | /** 24 | * @param {number} wait 25 | * @param {number=} maxWait 26 | */ 27 | constructor(wait, maxWait = Infinity) { 28 | this.map = new Map(); 29 | this.deleteTriggers_ = new Map(); 30 | this.wait_ = wait; 31 | this.maxWait_ = maxWait; 32 | } 33 | 34 | /** 35 | * @param {key} key 36 | * @param {T} value 37 | */ 38 | set(key, value) { 39 | this.map.set(key, value); 40 | 41 | let deleter = this.deleteTriggers_.get(key); 42 | if (!deleter) { 43 | deleter = debounce( 44 | () => { 45 | this.delete(key); 46 | }, 47 | this.wait_, 48 | { maxWait: this.maxWait_ } 49 | ); 50 | 51 | this.deleteTriggers_.set(key, deleter); 52 | } 53 | deleter(); 54 | } 55 | 56 | /** 57 | * @param {key} key 58 | * @return {T} 59 | */ 60 | get(key) { 61 | const deleter = this.deleteTriggers_.get(key); 62 | if (deleter) { 63 | deleter(); 64 | } 65 | return this.map.get(key); 66 | } 67 | 68 | /** 69 | * @param {key} key 70 | */ 71 | delete(key) { 72 | if (!this.has(key)) { 73 | return; 74 | } 75 | 76 | const value = this.map.get(key); 77 | if (value && value.destroy) { 78 | value.destroy(); 79 | } 80 | this.map.delete(key); 81 | 82 | const deleter = this.deleteTriggers_.get(key); 83 | deleter.cancel(); 84 | this.deleteTriggers_.delete(key); 85 | } 86 | 87 | /** 88 | * @param {key} key 89 | * @return {boolean} 90 | */ 91 | has(key) { 92 | return this.map.has(key); 93 | } 94 | 95 | /** 96 | * @return {number} 97 | */ 98 | get size() { 99 | return this.map.size; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /utils/log-target.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS-IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | /** 16 | * @fileoverview 17 | * Constructs the service bucket name and version identifier reported to 18 | * Stackdriver Logging. 19 | */ 20 | 21 | import * as logs from './log.js'; 22 | import { humanRtv } from './rtv/human-rtv.js'; 23 | import { releaseChannels } from './rtv/release-channels.js'; 24 | 25 | const GOOGLE_AMP_CACHE_REGEX = new RegExp( 26 | '^https://cdn\\.ampproject.org/|' + 27 | '\\.cdn\\.ampproject\\.org/|' + 28 | '\\.ampproject\\.net/', 29 | 'i' 30 | ); 31 | 32 | export class LoggingTarget { 33 | constructor(referrer, reportingParams) { 34 | this.opts = { referrer, ...reportingParams }; 35 | this.log = this.getLog(); 36 | } 37 | 38 | /** Select which error logging project to report to. */ 39 | getLog() { 40 | const { assert, expected, message, runtime } = this.opts; 41 | 42 | if ( 43 | runtime === 'inabox' || 44 | message.includes('Signing service error for google') 45 | ) { 46 | return logs.ads; 47 | } 48 | 49 | if (assert) { 50 | return logs.users; 51 | } 52 | 53 | if (expected) { 54 | return logs.expected; 55 | } 56 | 57 | return logs.errors; 58 | } 59 | 60 | /** Construct the service bucket name for Stackdriver logging. */ 61 | get serviceName() { 62 | const { cdn, expected, referrer, version } = this.opts; 63 | const rtvPrefix = version.substr(0, 2); 64 | 65 | const name = [releaseChannels[rtvPrefix]?.group ?? '[Unspecified Channel]']; 66 | if (GOOGLE_AMP_CACHE_REGEX.test(referrer)) { 67 | name.push('Google Cache'); 68 | } else if (cdn) { 69 | name.push(`Publisher Origin (${cdn})`); 70 | } else { 71 | name.push(`Publisher Origin (CDN not reported)`); 72 | } 73 | 74 | if (expected && this.getLog() !== logs.expected) { 75 | // Expected errors are split out of the main bucket, but are present for 76 | // user and inabox errors. 77 | name.push('(Expected)'); 78 | } 79 | 80 | return name.join(' > '); 81 | } 82 | 83 | /** Determine the version identifier to report to Stackdriver logging. */ 84 | get versionId() { 85 | return humanRtv(this.opts.version); 86 | } 87 | 88 | /** Determine throttle level for error type. */ 89 | get throttleRate() { 90 | const { assert, binaryType, canary, expected, prethrottled } = this.opts; 91 | let throttleRate = 1; 92 | 93 | // Throttle errors from Stable, unless pre-throttled on the client. 94 | if (!canary && binaryType === 'production' && !prethrottled) { 95 | throttleRate /= 10; 96 | } 97 | 98 | // Throttle user errors. 99 | if (assert) { 100 | throttleRate /= 10; 101 | } 102 | 103 | if (expected) { 104 | throttleRate /= 10; 105 | } 106 | 107 | return throttleRate; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /utils/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview exports log object to enable stubbing of write method 19 | */ 20 | 21 | import { Logging } from '@google-cloud/logging'; 22 | 23 | export const generic = new Logging({ 24 | projectId: 'amp-error-reporting', 25 | }).log('stderr'); 26 | 27 | const jsLog = (projectId) => 28 | new Logging({ projectId }).log('javascript.errors'); 29 | 30 | export const errors = jsLog('amp-error-reporting'); 31 | export const users = jsLog('amp-error-reporting-user'); 32 | export const ads = jsLog('amp-error-reporting-ads'); 33 | export const expected = jsLog('amp-error-reporting-expected'); 34 | -------------------------------------------------------------------------------- /utils/requests/extract-reporting-params.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS-IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | /** 16 | * @fileoverview 17 | * Parses options from the error reporting URL params into an object with clear 18 | * keys and sanitized values. 19 | */ 20 | import { stringify } from './query-string.js'; 21 | import safeDecodeURIComponent from 'safe-decode-uri-component'; 22 | 23 | export function extractReportingParams(params) { 24 | const boolProp = (key) => params[key] === '1'; 25 | const strProp = (key) => params[key]?.trim() ?? ''; 26 | 27 | return { 28 | assert: boolProp('a'), 29 | binaryType: strProp('bt'), 30 | canary: boolProp('ca'), 31 | cdn: strProp('cdn'), 32 | debug: boolProp('debug'), 33 | expected: boolProp('ex'), 34 | message: safeDecodeURIComponent(strProp('m')), 35 | buildQueryString: () => stringify(params), 36 | prethrottled: boolProp('pt'), 37 | runtime: params.rt, 38 | singlePassType: params.spt, 39 | stacktrace: safeDecodeURIComponent(strProp('s')), 40 | thirdParty: boolProp('3p'), 41 | version: params.v, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /utils/requests/parse-error-handling.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { StatusCodes } from 'http-status-codes'; 18 | import * as logs from '../log.js'; 19 | 20 | let timestamp = 0; 21 | const fiveMin = 5 * 60 * 1000; 22 | const truncatedLength = 2 * 1024; // 2kb 23 | 24 | export function parseErrorHandling(err, req, res, next) { 25 | if (err.statusCode !== StatusCodes.REQUEST_TOO_LONG) { 26 | // Some other error. Let it propagate. 27 | return next(err); 28 | } 29 | 30 | // Log every 5 min 31 | const now = Date.now(); 32 | if (now > timestamp + fiveMin) { 33 | read(req, res); 34 | timestamp = now; 35 | } 36 | } 37 | 38 | /** 39 | * @param {!Request} req 40 | * @param {!Response} res 41 | */ 42 | function read(req, res) { 43 | let message = ''; 44 | req.resume(); 45 | req.on('data', onData); 46 | req.on('end', onEnd); 47 | req.on('error', onEnd); 48 | 49 | /** 50 | * @param {Buffer} data 51 | */ 52 | function onData(data) { 53 | message += data; 54 | if (message.length >= truncatedLength) { 55 | message = message.slice(0, truncatedLength); 56 | onEnd(); 57 | } 58 | } 59 | /** */ 60 | function onEnd() { 61 | log(message); 62 | cleanup(); 63 | res.sendStatus(StatusCodes.REQUEST_TOO_LONG); 64 | } 65 | 66 | /** */ 67 | function cleanup() { 68 | req.removeListener('data', onData); 69 | req.removeListener('end', onEnd); 70 | req.removeListener('error', onEnd); 71 | } 72 | 73 | /** 74 | * @param {string} message 75 | */ 76 | function log(message) { 77 | const entry = logs.generic.entry( 78 | { 79 | labels: { 80 | 'appengine.googleapis.com/instance_name': process.env.GAE_INSTANCE, 81 | }, 82 | resource: { 83 | type: 'gae_app', 84 | labels: { 85 | module_id: process.env.GAE_SERVICE, 86 | version_id: process.env.GAE_VERSION, 87 | }, 88 | }, 89 | severity: 400, // Warning. 90 | }, 91 | { 92 | message: 'PayloadTooLargeError', 93 | context: { 94 | httpRequest: { 95 | method: req.method, 96 | url: req.originalUrl, 97 | userAgent: req.get('User-Agent'), 98 | referrer: req.get('Referrer'), 99 | body: message, 100 | }, 101 | }, 102 | } 103 | ); 104 | logs.generic.write(entry, (writeErr) => { 105 | if (writeErr) { 106 | console.error(writeErr); 107 | } 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /utils/requests/query-string.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS-IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | /** 16 | * Turns an object into a safe query string. 17 | * Note, this does not prepend a "?". 18 | * 19 | * @param {!Object} obj 20 | * @return {string} 21 | */ 22 | export function stringify(obj) { 23 | let string = ''; 24 | for (const prop in obj) { 25 | string += `&${encodeURIComponent(prop)}=${encodeURIComponent(obj[prop])}`; 26 | } 27 | 28 | return string.substring(1); 29 | } 30 | -------------------------------------------------------------------------------- /utils/rtv/human-rtv.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS-IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | import { releaseChannels } from './release-channels.js'; 16 | const RTV_REGEX = /^(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d\d)$/; 17 | 18 | export function humanRtv(rtv) { 19 | try { 20 | const [ 21 | unusedRtv, 22 | rtvPrefix, 23 | unusedYear, 24 | month, 25 | day, 26 | hour, 27 | minute, 28 | cherrypicks, 29 | ] = RTV_REGEX.exec(rtv); 30 | const channelName = 31 | rtvPrefix in releaseChannels 32 | ? releaseChannels[rtvPrefix].name 33 | : 'Unknown'; 34 | const cpCount = Number(cherrypicks); 35 | const fingerprint = `${hour}${minute}${cpCount ? `+${cpCount}` : ''}`; 36 | 37 | return `${month}-${day} ${channelName} (${fingerprint})`; 38 | } catch { 39 | return rtv; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /utils/rtv/latest-rtv.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The AMP Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS-IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | /** 16 | * @fileoverview Fetches the latest production RTV. This is used as a filter 17 | * to prevent reports for old errors from getting through. 18 | */ 19 | 20 | // TODO(@danielrozenberg): replace this with native `fetch` when `nock` supports it. 21 | import fetch from 'node-fetch'; 22 | import { Cache } from '../cache.js'; 23 | import { generic as genericLog } from '../log.js'; 24 | 25 | const url = 'https://cdn.ampproject.org/rtv/metadata'; 26 | const fiveMin = 5 * 60 * 1000; 27 | const fiftyMin = 50 * 60 * 1000; 28 | const cache = new Cache(fiveMin, fiftyMin); 29 | 30 | /** 31 | * Fetches current active RTVs. 32 | * 33 | * @returns {!Promise} list of active RTVs. 34 | */ 35 | export async function latestRtv() { 36 | if (cache.has(url)) { 37 | return cache.get(url); 38 | } 39 | 40 | try { 41 | const res = await fetch(url); 42 | const { ampRuntimeVersion, diversions, ltsRuntimeVersion } = 43 | await res.json(); 44 | const versions = [ 45 | ampRuntimeVersion, 46 | ltsRuntimeVersion, 47 | ...diversions, 48 | ].filter(Boolean); 49 | 50 | cache.set(url, versions); 51 | return versions; 52 | } catch (err) { 53 | try { 54 | genericLog.write( 55 | genericLog.entry( 56 | { 57 | labels: { 58 | 'appengine.googleapis.com/instance_name': 59 | process.env.GAE_INSTANCE, 60 | }, 61 | resource: { 62 | type: 'gae_app', 63 | labels: { 64 | module_id: process.env.GAE_SERVICE, 65 | version_id: process.env.GAE_VERSION, 66 | }, 67 | }, 68 | severity: 500, // Error. 69 | }, 70 | `failed to fetch RTV metadata: ${err.message}` 71 | ) 72 | ); 73 | } catch (writeErr) { 74 | console.warn('Error logging RTV fetch error: ', writeErr); 75 | } 76 | 77 | cache.delete(url); 78 | return []; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /utils/rtv/release-channels.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 The AMP Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS-IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | /** 16 | * @fileoverview 17 | * Provides information about release channel prefixes. 18 | */ 19 | 20 | export const releaseChannels = { 21 | '00': { group: '1%', name: 'Experimental' }, 22 | '01': { group: 'Production', name: 'Stable' }, 23 | '02': { group: 'Production', name: 'Control' }, 24 | '03': { group: '1%', name: 'Beta' }, 25 | '04': { group: 'Nightly', name: 'Nightly' }, 26 | '05': { group: 'Nightly', name: 'Nightly-Control' }, 27 | 10: { group: 'Experiments', name: 'Experiment-A' }, 28 | 11: { group: 'Experiments', name: 'Experiment-B' }, 29 | 12: { group: 'Experiments', name: 'Experiment-C' }, 30 | // Ads error reporting will get all of the below channels, so the service 31 | // bucket names can be more verbose. 32 | 20: { group: 'Inabox-Control-A', name: 'Inabox-Control-A' }, 33 | 21: { group: 'Inabox-Experiment-A', name: 'Inabox-Experiment-A' }, 34 | 22: { group: 'Inabox-Control-B', name: 'Inabox-Control-B' }, 35 | 23: { group: 'Inabox-Experiment-B', name: 'Inabox-Experiment-B' }, 36 | 24: { group: 'Inabox-Control-C', name: 'Inabox-Control-C' }, 37 | 25: { group: 'Inabox-Experiment-C', name: 'Inabox-Experiment-C' }, 38 | }; 39 | -------------------------------------------------------------------------------- /utils/stacktrace/frame.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS-IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | /** 16 | * Dedupes source paths. 17 | * Temporary fix until #27665 fixes the sourcemaps. 18 | * @param {string} source 19 | * @return {string} 20 | */ 21 | function dedupeSource(source) { 22 | return ( 23 | source && 24 | source.replace(/(ampproject\/amphtml\/\d+)(\/.+)\2(\/[^/]+)$/, '$1$2$3') 25 | ); 26 | } 27 | 28 | /** 29 | * Represents a single frame in a stack trace. 30 | */ 31 | export class Frame { 32 | /** 33 | * @param {string} name The context name of the frame 34 | * @param {string} source The file source of the frame 35 | * @param {string} line The file line of the frame 36 | * @param {string} column The file column of the frame 37 | */ 38 | constructor(name, source, line, column) { 39 | this.name = name; 40 | this.source = dedupeSource(source); 41 | this.line = parseInt(line, 10); 42 | this.column = parseInt(column, 10); 43 | } 44 | 45 | /** 46 | * Returns a (Chrome formatted) string of the frame 47 | * @return {string} 48 | */ 49 | toString() { 50 | const name = this.name; 51 | const location = `${this.source}:${this.line}:${this.column}`; 52 | 53 | if (name) { 54 | return ` at ${name} (${location})`; 55 | } 56 | return ` at ${location}`; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /utils/stacktrace/should-ignore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview A helper that blacklists some stacks from being reported. 19 | */ 20 | 21 | const errorsToIgnore = [ 22 | 'stop_youtube', 23 | 'null%20is%20not%20an%20object%20(evaluating%20%27elt.parentNode%27)', 24 | ]; 25 | const JS_REGEX = /\.m?js$/; 26 | 27 | /** 28 | * @param {!Array} stack 29 | * @return {boolean} True if its a non JS stack trace 30 | */ 31 | function isNonJSStackTrace(stack) { 32 | return !stack.every(({ source }) => { 33 | return JS_REGEX.test(source); 34 | }); 35 | } 36 | 37 | /** 38 | * @param {string} message 39 | * @return {boolean} 40 | */ 41 | function includesBlacklistedError(message) { 42 | return errorsToIgnore.some((msg) => message.includes(msg)); 43 | } 44 | 45 | /** 46 | * @param {string} message 47 | * @param {!Array} stack 48 | * @return {boolean} 49 | */ 50 | export function shouldIgnore(message, stack) { 51 | return includesBlacklistedError(message) || isNonJSStackTrace(stack); 52 | } 53 | -------------------------------------------------------------------------------- /utils/stacktrace/standardize-stack-trace.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP HTML Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS-IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Frame } from './frame.js'; 18 | 19 | const lineColumnNumbersRegex = '([^ \\n]+):(\\d+):(\\d+)'; 20 | const chromeFrame = new RegExp( 21 | `^\\s*at (?:` + 22 | `${lineColumnNumbersRegex}|(.+)? \\(${lineColumnNumbersRegex}\\))$`, 23 | 'gm' 24 | ); 25 | const safariFrame = /^\s*(?:([^@\n]*)@)?([^@\n]+):(\d+):(\d+)$/gm; 26 | 27 | /** 28 | * Removes the .br extension, since the file is expected to match the 29 | * regular .js file. 30 | * @param {string} source 31 | * @return {string} 32 | */ 33 | function brotliToJs(source) { 34 | return source.replace(/\.js\.br$/, '.js'); 35 | } 36 | 37 | /** 38 | * Parses a Chrome formatted stack trace string. 39 | * @param {string} stack 40 | * @return {!Array} 41 | */ 42 | function chromeStack(stack) { 43 | const frames = []; 44 | let match; 45 | 46 | while ((match = chromeFrame.exec(stack))) { 47 | frames.push( 48 | new Frame( 49 | match[4] || '', 50 | brotliToJs(match[1] || match[5]), 51 | match[2] || match[6], 52 | match[3] || match[7] 53 | ) 54 | ); 55 | } 56 | 57 | return frames; 58 | } 59 | 60 | /** 61 | * Parses a Safari formatted stack trace string. 62 | * @param {string} stack 63 | * @return {!Array} 64 | */ 65 | function safariStack(stack) { 66 | const frames = []; 67 | let match; 68 | 69 | while ((match = safariFrame.exec(stack))) { 70 | frames.push( 71 | new Frame(match[1] || '', brotliToJs(match[2]), match[3], match[4]) 72 | ); 73 | } 74 | 75 | return frames; 76 | } 77 | 78 | /** 79 | * Standardizes Chrome/IE and Safari/Firefox stack traces into an array of 80 | * frame objects. 81 | * 82 | * If there are no parsable stack frames, a default frame will be generated 83 | * based on the error message. 84 | * @param {string} stack 85 | * @param {string} message 86 | * @return {!Array} The converted stack trace. 87 | */ 88 | export function standardizeStackTrace(stack, message) { 89 | let frames; 90 | if (chromeFrame.test(stack)) { 91 | chromeFrame.lastIndex = 0; 92 | frames = chromeStack(stack); 93 | } else { 94 | frames = safariStack(stack); 95 | } 96 | 97 | if (frames.length === 0) { 98 | // Generate a unique filename based on the message's words. 99 | // This is to prevent StackDriver from grouping different error reports 100 | // together. 101 | const words = message.match(/\w+/g) || ['unknown']; 102 | const file = `${words.join('-').toLowerCase()}.js`; 103 | frames.push(new Frame('', file, '1', '1')); 104 | } 105 | 106 | return frames; 107 | } 108 | -------------------------------------------------------------------------------- /utils/stacktrace/unminify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 The AMP Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS-IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | /** 16 | * @fileoverview Convert's stacktrace line, column number and file references 17 | * from minified to unminified. Caches requests for and source maps once 18 | * obtained. 19 | */ 20 | 21 | import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping'; 22 | // TODO(@danielrozenberg): replace this with native `fetch` when `nock` supports it. 23 | import fetch from 'node-fetch'; 24 | 25 | import { Cache } from '../cache.js'; 26 | import { Frame } from './frame.js'; 27 | import { generic as genericLog } from '../log.js'; 28 | 29 | const twoWeeks = 2 * 7 * 24 * 60 * 60 * 1000; 30 | const oneMinute = 60 * 1000; 31 | 32 | /** @type {Cache} */ 33 | const traceMapCache = new Cache(twoWeeks); 34 | /** @type {Cache>} */ 35 | const pendingRequests = new Cache(oneMinute); 36 | 37 | const cdnJsRegex = new RegExp( 38 | // Require the CDN URL origin at the beginning. 39 | '^(https://cdn\\.ampproject\\.org)' + 40 | // Allow, but don't require, RTV. 41 | '(?:/rtv/(\\d{2}\\d{13,}))?' + 42 | // Require text "/v" followed by digits 43 | '(/(?:amp4ads-v|v)\\d+' + 44 | // Allow, but don't require, an extension under the v0 directory. 45 | '(?:/(.+?))?' + 46 | ')' + 47 | // Allow, but don't require, "-module" and "-nomodule". 48 | '(-(?:module|nomodule))?' + 49 | // Require ".js" or ".mjs" extension, optionally followed by ".br". 50 | '(\\.(m)?js)(\\.br)?$' 51 | ); 52 | 53 | /** 54 | * For stack frames that are not CDN JS, we do not attempt to load a 55 | * real SourceMapConsumer. 56 | */ 57 | const nilConsumer = new TraceMap({ 58 | version: 3, 59 | sources: [], 60 | mappings: [], 61 | }); 62 | 63 | /** 64 | * Formats unversioned CDN JS files into the versioned url 65 | * @param {string} url 66 | * @param {string} version 67 | * @return {string} 68 | */ 69 | export function normalizeCdnJsUrl(url, version) { 70 | const match = cdnJsRegex.exec(url); 71 | if (!match) { 72 | return; 73 | } 74 | 75 | const [ 76 | unused_fullMatch, 77 | origin, 78 | rtv = version, 79 | pathname, 80 | ampExtension, 81 | module = '', 82 | ext, 83 | /* brotli, */ 84 | ] = match; 85 | 86 | // We explicitly forbid the experiments and validator "extensions" inside 87 | // the v0 directory. 88 | if (ampExtension === 'experiments' || ampExtension === 'validator') { 89 | return ''; 90 | } 91 | 92 | const normModule = module === '-nomodule' ? '' : module; 93 | 94 | return `${origin}/rtv/${rtv}${pathname}${normModule}${ext}.map`; 95 | } 96 | 97 | /** 98 | * @param {!Frame} frame 99 | * @param {Object} consumer 100 | * @return {!Frame} Stack trace frame with column, line number and file name 101 | * references unminified. 102 | */ 103 | function unminifyFrame(frame, consumer) { 104 | const { column, line, name, source } = originalPositionFor(consumer, { 105 | line: frame.line, 106 | column: frame.column, 107 | }); 108 | 109 | if (!source) { 110 | return frame; 111 | } 112 | 113 | return new Frame(name, source, line, column); 114 | } 115 | 116 | /** 117 | * @param {string} url 118 | * @return {Promise} Promise that resolves to a source map. 119 | */ 120 | async function getSourceMapFromNetwork(url) { 121 | try { 122 | const res = await fetch(url); 123 | const consumer = new TraceMap(await res.json()); 124 | traceMapCache.set(url, consumer); 125 | return consumer; 126 | } catch (err) { 127 | try { 128 | genericLog.write( 129 | genericLog.entry( 130 | { 131 | labels: { 132 | 'appengine.googleapis.com/instance_name': 133 | process.env.GAE_INSTANCE, 134 | }, 135 | resource: { 136 | type: 'gae_app', 137 | labels: { 138 | module_id: process.env.GAE_SERVICE, 139 | version_id: process.env.GAE_VERSION, 140 | }, 141 | }, 142 | severity: 500, // Error. 143 | }, 144 | { 145 | message: 'failed retrieving source map', 146 | context: { 147 | url, 148 | message: err.message, 149 | stack: err.stack, 150 | }, 151 | } 152 | ) 153 | ); 154 | } catch (writeErr) { 155 | console.error(writeErr); 156 | } 157 | throw err; 158 | } 159 | } 160 | 161 | /** 162 | * @param {Frame[]} stack 163 | * @param {string} version 164 | * @return {Promise} Array of promises that resolve to source maps. 165 | */ 166 | async function extractSourceMaps(stack, version) { 167 | const sourceMaps = stack.map(({ source }) => { 168 | const sourceMapUrl = normalizeCdnJsUrl(source, version); 169 | 170 | if (!sourceMapUrl) { 171 | return nilConsumer; 172 | } 173 | 174 | if (traceMapCache.has(sourceMapUrl)) { 175 | return traceMapCache.get(sourceMapUrl); 176 | } 177 | 178 | if (!pendingRequests.has(sourceMapUrl)) { 179 | pendingRequests.set(sourceMapUrl, getSourceMapFromNetwork(sourceMapUrl)); 180 | } 181 | return pendingRequests.get(sourceMapUrl); 182 | }); 183 | return Promise.all(sourceMaps); 184 | } 185 | 186 | /** 187 | * @param {Frame[]} stack 188 | * @param {string} version 189 | * @return {Promise} Promise that resolves to unminified stack trace. 190 | */ 191 | export async function unminify(stack, version) { 192 | try { 193 | const consumers = await extractSourceMaps(stack, version); 194 | return stack.map((frame, i) => unminifyFrame(frame, consumers[i])); 195 | } catch (unused) { 196 | return stack; 197 | } 198 | } 199 | --------------------------------------------------------------------------------