fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,139 @@
const DOMAIN_IN_URL_REGEX = /:\/\/(\S*?)(:\d+)?(\/|$)/
const DOMAIN_CHARACTERS = /([a-z0-9.-]+\.[a-z0-9]+|localhost)/i
const IP_REGEX = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
const ROOT_DOMAIN_REGEX = /[^.]+\.([^.]+|(gov|com|co|ne)\.\w{2})$/i
function getDomainFromOriginOrURL(originOrURL) {
if (typeof originOrURL !== 'string') return null
if (originOrURL.length > 10000 || originOrURL.startsWith('data:')) return null
if (DOMAIN_IN_URL_REGEX.test(originOrURL)) return originOrURL.match(DOMAIN_IN_URL_REGEX)[1]
if (DOMAIN_CHARACTERS.test(originOrURL)) return originOrURL.match(DOMAIN_CHARACTERS)[0]
return null
}
function getRootDomain(originOrURL) {
const domain = getDomainFromOriginOrURL(originOrURL)
if (!domain) return null
if (IP_REGEX.test(domain)) return domain
const match = domain.match(ROOT_DOMAIN_REGEX)
return (match && match[0]) || domain
}
function sliceSubdomainFromDomain(domain, rootDomain) {
if (domain.length <= rootDomain.length) return domain
return domain
.split('.')
.slice(1)
.join('.')
}
function getEntityInDataset(entityByDomain, entityBySubDomain, entityByRootDomain, originOrURL) {
const domain = getDomainFromOriginOrURL(originOrURL)
const rootDomain = getRootDomain(domain)
if (!domain || !rootDomain) return undefined
if (entityByDomain.has(domain)) return entityByDomain.get(domain)
for (
let subdomain = domain;
subdomain.length > rootDomain.length;
subdomain = sliceSubdomainFromDomain(subdomain, rootDomain)
) {
if (entityBySubDomain.has(subdomain)) return entityBySubDomain.get(subdomain)
}
if (entityByRootDomain.has(rootDomain)) return entityByRootDomain.get(rootDomain)
return undefined
}
function getProductInDataset(entityByDomain, entityBySubDomain, entityByRootDomain, originOrURL) {
const entity = getEntityInDataset(
entityByDomain,
entityBySubDomain,
entityByRootDomain,
originOrURL
)
const products = entity && entity.products
if (!products) return undefined
if (typeof originOrURL !== 'string') return undefined
for (const product of products) {
for (const pattern of product.urlPatterns) {
if (pattern instanceof RegExp && pattern.test(originOrURL)) return product
if (typeof pattern === 'string' && originOrURL.includes(pattern)) return product
}
}
return undefined
}
function cloneEntities(entities) {
return entities.map(entity_ => {
const entity = {
company: entity_.name,
categories: [entity_.category],
...entity_,
}
const products = (entity_.products || []).map(product => ({
company: entity.company,
category: entity.category,
categories: [entity.category],
facades: [],
...product,
urlPatterns: (product.urlPatterns || []).map(s =>
s.startsWith('REGEXP:') ? new RegExp(s.slice('REGEXP:'.length)) : s
),
}))
entity.products = products
return entity
})
}
function createAPIFromDataset(entities_) {
const entities = cloneEntities(entities_)
const entityByDomain = new Map()
const entityByRootDomain = new Map()
const entityBySubDomain = new Map()
for (const entity of entities) {
entity.totalExecutionTime = Number(entity.totalExecutionTime) || 0
entity.totalOccurrences = Number(entity.totalOccurrences) || 0
entity.averageExecutionTime = entity.totalExecutionTime / entity.totalOccurrences
for (const domain of entity.domains) {
if (entityByDomain.has(domain)) {
const duplicate = entityByDomain.get(domain)
throw new Error(`Duplicate domain ${domain} (${entity.name} and ${duplicate.name})`)
}
entityByDomain.set(domain, entity)
const rootDomain = getRootDomain(domain)
if (domain.startsWith('*.')) {
const wildcardDomain = domain.slice(2)
if (wildcardDomain === rootDomain) entityByRootDomain.set(rootDomain, entity)
else entityBySubDomain.set(wildcardDomain, entity)
}
}
}
for (const [rootDomain, entity] of entityByRootDomain.entries()) {
if (!entity) entityByRootDomain.delete(rootDomain)
}
const getEntity = getEntityInDataset.bind(
null,
entityByDomain,
entityBySubDomain,
entityByRootDomain
)
const getProduct = getProductInDataset.bind(
null,
entityByDomain,
entityBySubDomain,
entityByRootDomain
)
return {getEntity, getProduct, getRootDomain, entities}
}
module.exports = {createAPIFromDataset}

View File

@@ -0,0 +1,44 @@
const {createAPIFromDataset} = require('./create-entity-finder-api.js')
describe('getEntity', () => {
let api
beforeEach(() => {
api = createAPIFromDataset([
{
name: 'Domain',
domains: ['*.example.com', '*.example.co.uk'],
},
{
name: 'Subdomain',
domains: ['*.sub.example.com', '*.sub.example.co.uk'],
},
{
name: 'Subsubdomain',
domains: ['very.specific.example.com'],
},
])
})
it('should find direct domains', () => {
expect(api.getEntity('https://very.specific.example.com/path').name).toEqual('Subsubdomain')
})
it('should find wildcard subdomains', () => {
expect(api.getEntity('https://foo.sub.example.com/path').name).toEqual('Subdomain')
expect(api.getEntity('https://bar.sub.example.com/path').name).toEqual('Subdomain')
expect(api.getEntity('https://baz.bar.sub.example.com/path').name).toEqual('Subdomain')
expect(api.getEntity('https://foo.sub.example.co.uk/path').name).toEqual('Subdomain')
expect(api.getEntity('https://bar.sub.example.co.uk/path').name).toEqual('Subdomain')
expect(api.getEntity('https://baz.bar.sub.example.co.uk/path').name).toEqual('Subdomain')
})
it('should find wildcard domains', () => {
expect(api.getEntity('https://foo.example.com/path').name).toEqual('Domain')
expect(api.getEntity('https://bar.example.com/path').name).toEqual('Domain')
expect(api.getEntity('https://baz.bar.example.com/path').name).toEqual('Domain')
expect(api.getEntity('https://foo.example.co.uk/path').name).toEqual('Domain')
expect(api.getEntity('https://bar.example.co.uk/path').name).toEqual('Domain')
expect(api.getEntity('https://baz.bar.example.co.uk/path').name).toEqual('Domain')
})
})

27
node_modules/third-party-web/lib/entities.test.js generated vendored Normal file
View File

@@ -0,0 +1,27 @@
const _ = require('lodash')
const {entities, getRootDomain, getEntity} = require('./index.js')
describe('Entities', () => {
it('should not have duplicate names', () => {
for (let i = 0; i < entities.length; i++) {
for (let j = i + 1; j < entities.length; j++) {
const nameA = entities[i].name.replace(/\s+/, '').toLowerCase()
const nameB = entities[j].name.replace(/\s+/, '').toLowerCase()
if (nameA !== nameB) continue
expect(entities[i]).toBe(entities[j])
}
}
})
it('should not have non-supported wilcards', () => {
for (const entity of entities) {
for (const domain of entity.domains) {
// Wildcards must be like `*.blah.com`
// Wildcards cannot be `*blah.com` or `blah*.com`
expect(domain).toEqual(expect.not.stringMatching(/\w\*/))
expect(domain).toEqual(expect.not.stringMatching(/\*\w/))
}
}
})
})

34
node_modules/third-party-web/lib/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,34 @@
export interface IFacade {
name: string
repo: string
}
export interface IProduct {
name: string
company: string
homepage?: string
category: string
/** @deprecated - Use `category` instead. */
categories: string[]
urlPatterns?: string[]
facades?: IFacade[]
}
export interface IEntity {
name: string
company: string
homepage?: string
category: string
/** @deprecated - Use `category` instead. */
categories: string[]
domains: string[]
products?: IProduct[]
averageExecutionTime: number
totalExecutionTime: number
totalOccurrences: number
}
export declare const entities: IEntity[]
export declare function getRootDomain(url: string): string
export declare function getEntity(url: string): IEntity | undefined
export declare function getProduct(url: string): IProduct | undefined

3
node_modules/third-party-web/lib/index.js generated vendored Normal file
View File

@@ -0,0 +1,3 @@
const {createAPIFromDataset} = require('./create-entity-finder-api.js')
const entities = require('../dist/entities.json')
module.exports = createAPIFromDataset(entities)

246
node_modules/third-party-web/lib/index.test.js generated vendored Normal file
View File

@@ -0,0 +1,246 @@
const fs = require('fs')
const path = require('path')
const {entities, getRootDomain, getEntity, getProduct} = require('./index.js')
describe('getRootDomain', () => {
it('works for IP addresses', () => {
expect(getRootDomain('8.8.8.8')).toEqual('8.8.8.8')
expect(getRootDomain('192.168.0.1')).toEqual('192.168.0.1')
})
it('works for basic domains', () => {
expect(getRootDomain('cdn.cnn.com')).toEqual('cnn.com')
expect(getRootDomain('www.hulce.photography')).toEqual('hulce.photography')
expect(getRootDomain('api.supercool.io')).toEqual('supercool.io')
})
it('works for country-tlds', () => {
expect(getRootDomain('content.yahoo.co.jp')).toEqual('yahoo.co.jp')
expect(getRootDomain('go.visit.gov.in')).toEqual('visit.gov.in')
})
it('works for URLs', () => {
expect(getRootDomain('https://content.yahoo.co.jp/path/?query=param')).toEqual('yahoo.co.jp')
expect(getRootDomain('https://a.b.c.it/path/?query=param&two=2')).toEqual('c.it')
expect(getRootDomain('https://foo.bar:433/path/?query=param&two=2')).toEqual('foo.bar')
})
it('works for localhost', () => {
expect(getRootDomain('https://localhost:8080/path/?query=param')).toEqual('localhost')
expect(getRootDomain('https://localhost/path/?query=param&two=2')).toEqual('localhost')
expect(getRootDomain('localhost:9000/path/?query=param&two=2')).toEqual('localhost')
expect(getRootDomain('localhost:1200')).toEqual('localhost')
})
it('works for wildcard domains', () => {
expect(getRootDomain('*.google.com')).toEqual('google.com')
expect(getRootDomain('*.yahoo.co.jp')).toEqual('yahoo.co.jp')
expect(getRootDomain('*.hulce.photography')).toEqual('hulce.photography')
})
it('runs on *massive* inputs', () => {
const massiveInput = '123456789'.repeat(100e3)
expect(getRootDomain(massiveInput)).toEqual(null)
})
it('runs on data URIs', () => {
const dataUri = 'data:image/gif;base64,R0lGODlhAQABAIAAAP8AADAAACwAAAAAAQABAAACAkQBADs='
expect(getRootDomain(dataUri)).toEqual(null)
})
it('returns null on invalid inputs', () => {
expect(getRootDomain('this is not a domain')).toEqual(null)
expect(getRootDomain('neither-is-this')).toEqual(null)
expect(getRootDomain('http://nor this')).toEqual(null)
})
})
describe('getEntity', () => {
it('works for direct domain usage', () => {
expect(getEntity('https://js.connect.facebook.net/lib.js')).toMatchInlineSnapshot(`
Object {
"averageExecutionTime": 537.7635411466493,
"categories": Array [
"social",
],
"category": "social",
"company": "Facebook",
"domains": Array [
"*.facebook.com",
"*.atlassbx.com",
"*.fbsbx.com",
"fbcdn-photos-e-a.akamaihd.net",
"*.facebook.net",
"*.fbcdn.net",
],
"examples": Array [
"www.facebook.com",
"connect.facebook.net",
"staticxx.facebook.com",
"static.xx.fbcdn.net",
"m.facebook.com",
"an.facebook.com",
"platform-lookaside.fbsbx.com",
],
"homepage": "https://www.facebook.com",
"name": "Facebook",
"products": Array [
Object {
"categories": Array [
"social",
],
"category": "social",
"company": "Facebook",
"facades": Array [
Object {
"name": "React Live Chat Loader",
"repo": "https://github.com/calibreapp/react-live-chat-loader",
},
],
"name": "Facebook Messenger Customer Chat",
"urlPatterns": Array [
/connect\\\\\\.facebook\\\\\\.net\\\\/\\.\\*\\\\/sdk\\\\/xfbml\\\\\\.customerchat\\\\\\.js/,
],
},
],
"totalExecutionTime": 1944372814,
"totalOccurrences": 3615665,
}
`)
})
it('works for inferred domain usage', () => {
expect(getEntity('https://unknown.typekit.net/fonts.css')).toMatchInlineSnapshot(`
Object {
"averageExecutionTime": 267.3227213797333,
"categories": Array [
"cdn",
],
"category": "cdn",
"company": "Adobe",
"domains": Array [
"*.typekit.com",
"*.typekit.net",
],
"examples": Array [
"use.typekit.net",
"p.typekit.net",
],
"homepage": "https://fonts.adobe.com/",
"name": "Adobe TypeKit",
"products": Array [],
"totalExecutionTime": 49692888,
"totalOccurrences": 185891,
}
`)
})
it('does not over-infer', () => {
expect(getEntity('https://unknown.gstatic.com/what')).toEqual(undefined)
})
it('only infers as a fallback', () => {
expect(getEntity('http://fbcdn-photos-e-a.akamaihd.net/1234.jpg').name).toEqual('Facebook')
expect(getEntity('http://unknown.akamaihd.net/1234.jpg').name).toEqual('Akamai')
})
it('runs on *massive* inputs', () => {
const massiveInput = '123456789'.repeat(100e3)
expect(getEntity(massiveInput)).toEqual(undefined)
})
it('runs on data URIs', () => {
const dataUri = 'data:image/gif;base64,R0lGODlhAQABAIAAAP8AADAAACwAAAAAAQABAAACAkQBADs='
expect(getEntity(dataUri)).toEqual(undefined)
})
it('supports multi-tennant domains', () => {
expect(getEntity('https://gemius.mgr.consensu.org/cmp/v2/stub.js').name).toEqual('Gemius CMP')
expect(
getEntity('https://quantcast.mgr.consensu.org/choice/KygWsHah2_7Qa/rssing.com/choice.js').name
).toEqual('Quantcast Choice')
expect(getEntity('https://static.quantcast.mgr.consensu.org/v50/cmpui-popup.js').name).toEqual(
'Quantcast Choice'
)
})
})
describe('getProduct', () => {
it('works on basic url', () => {
expect(getProduct('https://www.youtube.com/embed/alGcULGtiv8')).toMatchObject({
name: 'YouTube Embedded Player',
company: 'YouTube',
category: 'video',
categories: ['video'],
facades: [
{
name: 'Lite YouTube',
repo: 'https://github.com/paulirish/lite-youtube-embed',
},
{
name: 'Ngx Lite Video',
repo: 'https://github.com/karim-mamdouh/ngx-lite-video',
},
],
})
})
it('works on regex based', () => {
expect(
getProduct('https://connect.facebook.net/en_US/sdk/xfbml.customerchat.js')
).toMatchObject({
name: 'Facebook Messenger Customer Chat',
facades: [
{
name: 'React Live Chat Loader',
repo: 'https://github.com/calibreapp/react-live-chat-loader',
},
],
})
})
it('returns undefined when product does not match', () => {
expect(getProduct('https://js.connect.facebook.net/lib.js')).toEqual(undefined)
})
it('returns undefined with no products', () => {
expect(getProduct('https://unknown.typekit.net/fonts.css')).toEqual(undefined)
})
})
describe('build state', () => {
it('should use the complete entities set', () => {
const sourceOfTruthEntities = require('../data/entities.js')
expect(entities).toHaveLength(sourceOfTruthEntities.length)
})
it('should have all the same subsets in root as lib', () => {
const srcSizes = fs.readdirSync(path.join(__dirname, 'subsets'))
const dstSizes = fs.readdirSync(path.join(__dirname, '../')).filter(f => f.includes('-subset'))
expect(dstSizes).toHaveLength(srcSizes.length) // run `yarn build` if this fails
for (const file of dstSizes) {
if (file.endsWith('.js')) require(path.join(__dirname, '../', file))
}
})
})
it('should work on real web data', () => {
const urls = fs
.readFileSync(path.join(__dirname, '../data/random-urls.txt'), 'utf8')
.split('\n')
.filter(Boolean)
for (const url of urls) {
getEntity(url) // ensure it doesn't throw
}
const top1000 = urls.slice(0, 1000).map(url => {
const cleanedUrl = url.split('?')[0]
const entity = getEntity(url)
return `${entity && entity.name} - ${cleanedUrl}`
})
// It's expected that this snapshot will change as coverage changes.
expect(top1000).toMatchSnapshot()
})

View File

@@ -0,0 +1,36 @@
---
name: faqs
---
### I don't see entity X in the list. What's up with that?
This can be for one of several reasons:
1. The entity does not have references to their origin on at least 50 pages in the dataset.
1. The entity's origins have not yet been identified. See [How can I contribute?](#contribute)
### What is "Total Occurences"?
Total Occurrences is the number of pages on which the entity is included.
### How is the "Average Impact" determined?
The HTTP Archive dataset includes Lighthouse reports for each URL on mobile. Lighthouse has an audit called "bootup-time" that summarizes the amount of time that each script spent on the main thread. The "Average Impact" for an entity is the total execution time of scripts whose domain matches one of the entity's domains divided by the total number of pages that included the entity.
```
Average Impact = Total Execution Time / Total Occurrences
```
### How does Lighthouse determine the execution time of each script?
Lighthouse's bootup time audit attempts to attribute all toplevel main-thread tasks to a URL. A main thread task is attributed to the first script URL found in the stack. If you're interested in helping us improve this logic, see [Contributing](#contributing) for details.
### The data for entity X seems wrong. How can it be corrected?
Verify that the origins in `data/entities.js` are correct. Most issues will simply be the result of mislabelling of shared origins. If everything checks out, there is likely no further action and the data is valid. If you still believe there's errors, file an issue to discuss futher.
<a name="contribute"></a>
### How can I contribute?
Only about 90% of the third party script execution has been assigned to an entity. We could use your help identifying the rest! See [Contributing](#contributing) for details.

View File

@@ -0,0 +1,9 @@
---
name: goals
---
1. Quantify the impact of third party scripts on the web.
1. Identify the third party scripts on the web that have the greatest performance cost.
1. Give developers the information they need to make informed decisions about which third parties to include on their sites.
1. Incentivize responsible third party script behavior.
1. Make this information accessible and useful.

View File

@@ -0,0 +1,5 @@
---
name: methodology
---
[HTTP Archive](https://httparchive.org/) is an initiative that tracks how the web is built. Every month, ~4 million sites are crawled with [Lighthouse](https://github.com/GoogleChrome/lighthouse) on mobile. Lighthouse breaks down the total script execution time of each page and attributes the execution to a URL. Using [BigQuery](https://cloud.google.com/bigquery/), this project aggregates the script execution to the origin-level and assigns each origin to the responsible entity.

151
node_modules/third-party-web/lib/markdown/template.md generated vendored Normal file
View File

@@ -0,0 +1,151 @@
# [Third Party Web](https://www.thirdpartyweb.today/)
## Check out the shiny new web UI https://www.thirdpartyweb.today/
Data on third party entities and their impact on the web.
This document is a summary of which third party scripts are most responsible for excessive JavaScript execution on the web today.
## Table of Contents
1. [Goals](#goals)
1. [Methodology](#methodology)
1. [npm Module](#npm-module)
1. [Updates](#updates)
1. [Data](#data)
1. [Summary](#summary)
1. [How to Interpret](#how-to-interpret)
1. [Third Parties by Category](#by-category)
<%= category_table_of_contents %>
1. [Third Parties by Total Impact](#by-total-impact)
1. [Future Work](#future-work)
1. [FAQs](#faqs)
1. [Contributing](#contributing)
## Goals
<%= partials.goals %>
## Methodology
<%= partials.methodology %>
## npm Module
The entity classification data is available as an npm module.
```js
const {getEntity} = require('third-party-web')
const entity = getEntity('https://d36mpcpuzc4ztk.cloudfront.net/js/visitor.js')
console.log(entity)
// {
// "name": "Freshdesk",
// "homepage": "https://freshdesk.com/",
// "category": "customer-success",
// "domains": ["d36mpcpuzc4ztk.cloudfront.net"]
// }
```
## Updates
<%= updates_contents %>
## Data
### Summary
Across top ~4 million sites, ~2700 origins account for ~57% of all script execution time with the top 50 entities already accounting for ~47%. Third party script execution is the majority chunk of the web today, and it's important to make informed choices.
### How to Interpret
Each entity has a number of data points available.
1. **Usage (Total Number of Occurrences)** - how many scripts from their origins were included on pages
1. **Total Impact (Total Execution Time)** - how many seconds were spent executing their scripts across the web
1. **Average Impact (Average Execution Time)** - on average, how many milliseconds were spent executing each script
1. **Category** - what type of script is this
<a name="by-category"></a>
### Third Parties by Category
This section breaks down third parties by category. The third parties in each category are ranked from first to last based on the average impact of their scripts. Perhaps the most important comparisons lie here. You always need to pick an analytics provider, but at least you can pick the most well-behaved analytics provider.
#### Overall Breakdown
Unsurprisingly, ads account for the largest identifiable chunk of third party script execution.
![breakdown by category](./by-category.png)
<%= category_contents %>
<a name="by-total-impact"></a>
### Third Parties by Total Impact
This section highlights the entities responsible for the most script execution across the web. This helps inform which improvements would have the largest total impact.
<%= all_data %>
## Future Work
1. Introduce URL-level data for more fine-grained analysis, i.e. which libraries from Cloudflare/Google CDNs are most expensive.
1. Expand the scope, i.e. include more third parties and have greater entity/category coverage.
## FAQs
<%= partials.faqs %>
## Contributing
### Thanks
A **huge** thanks to [@simonhearne](https://twitter.com/simonhearne) and [@soulgalore](https://twitter.com/soulislove) for their assistance in classifying additional domains!
### Updating the Entities
The domain->entity mapping can be found in `data/entities.js`. Adding a new entity is as simple as adding a new array item with the following form.
```js
{
"name": "Facebook",
"homepage": "https://www.facebook.com",
"category": "social",
"domains": [
"*.facebook.com",
"*.fbcdn.net"
],
"examples": [
"www.facebook.com",
"connect.facebook.net",
"staticxx.facebook.com",
"static.xx.fbcdn.net",
"m.facebook.com"
]
}
```
### Updating Attribution Logic
The logic for attribution to individual script URLs can be found in the [Lighthouse repo](https://github.com/GoogleChrome/lighthouse). File an issue over there to discuss further.
### Updating the Data
This is now automated! Run `yarn start:update-ha-data` with a `gcp-credentials.json` file in the root directory of this project (look at `bin/automated-update.js` for the steps involved).
### Updating this README
This README is auto-generated from the templates `lib/` and the computed data. In order to update the charts, you'll need to make sure you have `cairo` installed locally in addition to `yarn install`.
```bash
# Install `cairo` and dependencies for node-canvas
brew install pkg-config cairo pango libpng jpeg giflib
# Build the requirements in this repo
yarn build
# Regenerate the README
yarn start
```
### Updating the website
The web code is located in `www/` directory of this repository. Open a PR to make changes.

View File

@@ -0,0 +1 @@
Huge props to [WordAds](https://wordads.co/) for reducing their impact from ~2.5s to ~200ms on average! A few entities are showing considerably less data this cycle (Media Math, Crazy Egg, DoubleVerify, Bootstrap CDN). Perhaps they've added new CDNs/hostnames that we haven't identified or the basket of sites in HTTPArchive has shifted away from their usage.

View File

@@ -0,0 +1 @@
Almost 2,000 entities tracked now across ~3,000+ domains! Huge props to [@simonhearne](https://twitter.com/simonhearne) for making this massive increase possible. Tag Managers have now been split out into their own category since they represented such a large percentage of the "Mixed / Other" category.

View File

@@ -0,0 +1 @@
Google Ads clarified that `www.googletagservices.com` serves more ad scripts than generic tag management, and it has been reclassified accordingly. This has dropped the overall Tag Management share considerably back down to its earlier position.

View File

@@ -0,0 +1,14 @@
A shortcoming of the attribution approach has been fixed. Total usage is now reported based on the number of _pages_ in the dataset that use the third-party, not the number of _scripts_. Correspondingly, all average impact times are now reported _per page_ rather than _per script_. Previously, a third party could appear to have a lower impact or be more popular simply by splitting their work across multiple files.
Third-parties that performed most of their work from a single script should see little to no impact from this change, but some entities have seen significant ranking movement. Hosting providers that host entire pages are, understandably, the most affected.
Some notable changes below:
| Third-Party | Previously (per-script) | Now (per-page) |
| ----------- | ----------------------- | -------------- |
| Beeketing | 137 ms | 465 ms |
| Sumo | 263 ms | 798 ms |
| Tumblr | 324 ms | 1499 ms |
| Yandex APIs | 393 ms | 1231 ms |
| Google Ads | 402 ms | 1285 ms |
| Wix | 972 ms | 5393 ms |

View File

@@ -0,0 +1 @@
Due to a change in HTTPArchive measurement which temporarily disabled site-isolation (out-of-process iframes), all of the third-parties whose work previously took place off the main-thread are now counted _on_ the main thread (and thus appear in our stats). This is most evident in the change to Google-owned properties such as YouTube and Doubleclick whose _complete_ cost are now captured.

View File

@@ -0,0 +1 @@
export * from '../index'

View File

@@ -0,0 +1,3 @@
const {createAPIFromDataset} = require('../create-entity-finder-api.js')
const entities = require('../../dist/entities-httparchive-nostats.json')
module.exports = createAPIFromDataset(entities)

View File

@@ -0,0 +1 @@
export * from '../index'

View File

@@ -0,0 +1,3 @@
const {createAPIFromDataset} = require('../create-entity-finder-api.js')
const entities = require('../../dist/entities-httparchive.json')
module.exports = createAPIFromDataset(entities)

View File

@@ -0,0 +1 @@
export * from '../index'

3
node_modules/third-party-web/lib/subsets/nostats.js generated vendored Normal file
View File

@@ -0,0 +1,3 @@
const {createAPIFromDataset} = require('../create-entity-finder-api.js')
const entities = require('../../dist/entities-nostats.json')
module.exports = createAPIFromDataset(entities)