diff --git a/packages/js/internal-js-tests/changelog/add-bump-stats-error b/packages/js/internal-js-tests/changelog/add-bump-stats-error
new file mode 100644
index 00000000000..e05b3a3218d
--- /dev/null
+++ b/packages/js/internal-js-tests/changelog/add-bump-stats-error
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add bumpStat to woocommerce-tracks mock
diff --git a/packages/js/internal-js-tests/composer.lock b/packages/js/internal-js-tests/composer.lock
new file mode 100644
index 00000000000..2d6e31137f4
--- /dev/null
+++ b/packages/js/internal-js-tests/composer.lock
@@ -0,0 +1,1059 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "e5c0cde068c27bf2648013b5cf25a2b4",
+ "packages": [],
+ "packages-dev": [
+ {
+ "name": "automattic/jetpack-changelogger",
+ "version": "v3.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Automattic/jetpack-changelogger.git",
+ "reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/8f63c829b8d1b0d7b1d5de93510d78523ed18959",
+ "reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6",
+ "symfony/console": "^3.4 || ^5.2 || ^6.0",
+ "symfony/process": "^3.4 || ^5.2 || ^6.0",
+ "wikimedia/at-ease": "^1.2 || ^2.0"
+ },
+ "require-dev": {
+ "wikimedia/testing-access-wrapper": "^1.0 || ^2.0",
+ "yoast/phpunit-polyfills": "1.0.4"
+ },
+ "bin": [
+ "bin/changelogger"
+ ],
+ "type": "project",
+ "extra": {
+ "autotagger": true,
+ "branch-alias": {
+ "dev-trunk": "3.3.x-dev"
+ },
+ "mirror-repo": "Automattic/jetpack-changelogger",
+ "version-constants": {
+ "::VERSION": "src/Application.php"
+ },
+ "changelogger": {
+ "link-template": "https://github.com/Automattic/jetpack-changelogger/compare/${old}...${new}"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Automattic\\Jetpack\\Changelog\\": "lib",
+ "Automattic\\Jetpack\\Changelogger\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.",
+ "support": {
+ "source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.3.0"
+ },
+ "time": "2022-12-26T13:49:01+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "1.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "513e0666f7216c7459170d56df27dfcefe1689ea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea",
+ "reference": "513e0666f7216c7459170d56df27dfcefe1689ea",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/1.1.2"
+ },
+ "time": "2021-11-05T16:50:12+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "5.4.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "cef62396a0477e94fc52e87a17c6e5c32e226b7f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/cef62396a0477e94fc52e87a17c6e5c32e226b7f",
+ "reference": "cef62396a0477e94fc52e87a17c6e5c32e226b7f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php73": "^1.9",
+ "symfony/polyfill-php80": "^1.16",
+ "symfony/service-contracts": "^1.1|^2|^3",
+ "symfony/string": "^5.1|^6.0"
+ },
+ "conflict": {
+ "psr/log": ">=3",
+ "symfony/dependency-injection": "<4.4",
+ "symfony/dotenv": "<5.1",
+ "symfony/event-dispatcher": "<4.4",
+ "symfony/lock": "<4.4",
+ "symfony/process": "<4.4"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0|2.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2",
+ "symfony/config": "^4.4|^5.0|^6.0",
+ "symfony/dependency-injection": "^4.4|^5.0|^6.0",
+ "symfony/event-dispatcher": "^4.4|^5.0|^6.0",
+ "symfony/lock": "^4.4|^5.0|^6.0",
+ "symfony/process": "^4.4|^5.0|^6.0",
+ "symfony/var-dumper": "^4.4|^5.0|^6.0"
+ },
+ "suggest": {
+ "psr/log": "For using the console logger",
+ "symfony/event-dispatcher": "",
+ "symfony/lock": "",
+ "symfony/process": ""
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Console\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Eases the creation of beautiful and testable command line interfaces",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "cli",
+ "command-line",
+ "console",
+ "terminal"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/console/tree/5.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-07-26T12:21:55+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "2.5.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "d36279a5a4bc7f3ca2c412839f10d7c0aa2c1a02"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/d36279a5a4bc7f3ca2c412839f10d7c0aa2c1a02",
+ "reference": "d36279a5a4bc7f3ca2c412839f10d7c0aa2c1a02",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.5-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/2.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-04-18T08:26:06+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "1.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "0424dff1c58f028c451efff2045f5d92410bd540"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
+ "reference": "0424dff1c58f028c451efff2045f5d92410bd540",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "default-branch": true,
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-31T15:07:36+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-grapheme",
+ "version": "1.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+ "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a",
+ "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "default-branch": true,
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's grapheme_* functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "grapheme",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-31T15:07:36+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-normalizer",
+ "version": "1.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+ "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb",
+ "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "default-branch": true,
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's Normalizer class and related functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "intl",
+ "normalizer",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-31T15:07:36+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "1.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "8740a072b86292957feb42703edde77fcfca84fb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8740a072b86292957feb42703edde77fcfca84fb",
+ "reference": "8740a072b86292957feb42703edde77fcfca84fb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "default-branch": true,
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/1.x"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-06-20T08:18:00+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php73",
+ "version": "1.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php73.git",
+ "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/ec444d3f3f6505bb28d11afa41e75faadebc10a1",
+ "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "default-branch": true,
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php73\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php73/tree/v1.30.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-31T15:07:36+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php80",
+ "version": "1.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
+ "reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "default-branch": true,
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-31T15:07:36+00:00"
+ },
+ {
+ "name": "symfony/process",
+ "version": "5.4.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/deedcb3bb4669cae2148bc920eafd2b16dc7c046",
+ "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Executes commands in sub-processes",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/process/tree/5.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-31T14:33:22+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "2.5.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "351fb560172c6972ffa169f4ffaea6d58a9de33b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/351fb560172c6972ffa169f4ffaea6d58a9de33b",
+ "reference": "351fb560172c6972ffa169f4ffaea6d58a9de33b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "psr/container": "^1.1",
+ "symfony/deprecation-contracts": "^2.1|^3"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2"
+ },
+ "suggest": {
+ "symfony/service-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.5-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/2.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-04-18T08:26:06+00:00"
+ },
+ {
+ "name": "symfony/string",
+ "version": "5.4.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "909cec913edea162a3b2836788228ad45fcab337"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/string/zipball/909cec913edea162a3b2836788228ad45fcab337",
+ "reference": "909cec913edea162a3b2836788228ad45fcab337",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-intl-grapheme": "~1.0",
+ "symfony/polyfill-intl-normalizer": "~1.0",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php80": "~1.15"
+ },
+ "conflict": {
+ "symfony/translation-contracts": ">=3.0"
+ },
+ "require-dev": {
+ "symfony/error-handler": "^4.4|^5.0|^6.0",
+ "symfony/http-client": "^4.4|^5.0|^6.0",
+ "symfony/translation-contracts": "^1.1|^2",
+ "symfony/var-exporter": "^4.4|^5.0|^6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\String\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "grapheme",
+ "i18n",
+ "string",
+ "unicode",
+ "utf-8",
+ "utf8"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/string/tree/5.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-07-20T18:38:32+00:00"
+ },
+ {
+ "name": "wikimedia/at-ease",
+ "version": "v2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/wikimedia/at-ease.git",
+ "reference": "e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/wikimedia/at-ease/zipball/e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33",
+ "reference": "e8ebaa7bb7c8a8395481a05f6dc4deaceab11c33",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.9"
+ },
+ "require-dev": {
+ "mediawiki/mediawiki-codesniffer": "35.0.0",
+ "mediawiki/minus-x": "1.1.1",
+ "ockcyp/covers-validator": "1.3.3",
+ "php-parallel-lint/php-console-highlighter": "0.5.0",
+ "php-parallel-lint/php-parallel-lint": "1.2.0",
+ "phpunit/phpunit": "^8.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/Wikimedia/Functions.php"
+ ],
+ "psr-4": {
+ "Wikimedia\\AtEase\\": "src/Wikimedia/AtEase/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Tim Starling",
+ "email": "tstarling@wikimedia.org"
+ },
+ {
+ "name": "MediaWiki developers",
+ "email": "wikitech-l@lists.wikimedia.org"
+ }
+ ],
+ "description": "Safe replacement to @ for suppressing warnings.",
+ "homepage": "https://www.mediawiki.org/wiki/at-ease",
+ "support": {
+ "source": "https://github.com/wikimedia/at-ease/tree/v2.1.0"
+ },
+ "time": "2021-02-27T15:53:37+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "dev",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "platform-overrides": {
+ "php": "7.4"
+ },
+ "plugin-api-version": "2.6.0"
+}
diff --git a/packages/js/internal-js-tests/src/mocks/woocommerce-tracks.js b/packages/js/internal-js-tests/src/mocks/woocommerce-tracks.js
index e85d0c75be2..f9e4051471d 100644
--- a/packages/js/internal-js-tests/src/mocks/woocommerce-tracks.js
+++ b/packages/js/internal-js-tests/src/mocks/woocommerce-tracks.js
@@ -3,4 +3,5 @@
module.exports = {
recordEvent: jest.fn(),
recordPageView: jest.fn(),
+ bumpStat: jest.fn(),
};
diff --git a/packages/js/remote-logging/changelog/add-bump-stats-error b/packages/js/remote-logging/changelog/add-bump-stats-error
new file mode 100644
index 00000000000..2a41a632378
--- /dev/null
+++ b/packages/js/remote-logging/changelog/add-bump-stats-error
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Track frequency of unhandled JS errors with MC Stats
diff --git a/packages/js/remote-logging/package.json b/packages/js/remote-logging/package.json
index 0950946edf8..8bdc1cb7ace 100644
--- a/packages/js/remote-logging/package.json
+++ b/packages/js/remote-logging/package.json
@@ -53,6 +53,7 @@
]
},
"dependencies": {
+ "@woocommerce/tracks": "workspace:*",
"@wordpress/hooks": "wp-6.0",
"debug": "^4.3.4",
"tracekit": "^0.4.6"
@@ -152,6 +153,9 @@
"node_modules/@woocommerce/eslint-plugin/configs",
"node_modules/@woocommerce/eslint-plugin/rules",
"node_modules/@woocommerce/eslint-plugin/index.js",
+ "node_modules/@woocommerce/tracks/build",
+ "node_modules/@woocommerce/tracks/build-module",
+ "node_modules/@woocommerce/tracks/build-types",
"package.json"
]
}
diff --git a/packages/js/remote-logging/src/remote-logger.ts b/packages/js/remote-logging/src/remote-logger.ts
index 6508bb3ab42..e88ccc6c76f 100644
--- a/packages/js/remote-logging/src/remote-logger.ts
+++ b/packages/js/remote-logging/src/remote-logger.ts
@@ -5,6 +5,7 @@ import debugFactory from 'debug';
import { getSetting } from '@woocommerce/settings';
import TraceKit from 'tracekit';
import { applyFilters } from '@wordpress/hooks';
+import { bumpStat } from '@woocommerce/tracks';
/**
* Internal dependencies
@@ -187,6 +188,9 @@ export class RemoteLogger {
return;
}
+ // Bump the stat for unhandled JS errors to track the frequency of these errors.
+ bumpStat( 'error', 'unhandled-js-errors' );
+
if ( this.isRateLimited() ) {
return;
}
diff --git a/packages/js/tracks/README.md b/packages/js/tracks/README.md
index edfdbab762e..a6d0fbf7341 100644
--- a/packages/js/tracks/README.md
+++ b/packages/js/tracks/README.md
@@ -27,8 +27,8 @@ recordEvent( 'page_view', { path } )
| Param | Type | Description |
| --- | --- | --- |
-| eventName | String
| The name of the event to record, don't include the `wcadmin_` prefix |
-| eventProperties | Object
| Event properties to include in the event |
+| eventName | `String` | The name of the event to record, don't include the `wcadmin_` prefix |
+| eventProperties | `Object` | Event properties to include in the event |
### queueRecordEvent( eventName, eventProperties )
@@ -38,22 +38,45 @@ This allows you to delay tracks events that would otherwise cause a race conditi
For example, when we trigger `wcadmin_tasklist_appearance_continue_setup` we're simultaneously moving the user to a new page via
`window.location`. This is an example of a race condition that should be avoided by enqueueing the event,
and therefore running it on the next pageview.
-
| Param | Type | Description |
| --- | --- | --- |
-| eventName | String
| The name of the event to record, don't include the `wcadmin_` prefix |
-| eventProperties | Object
| Event properties to include in the event |
+| eventName | `String` | The name of the event to record, don't include the `wcadmin_` prefix |
+| eventProperties | `Object` | Event properties to include in the event |
-### recordPageView( eventName, eventProperties )
+### recordPageView( path, extraProperties )
Record a page view to Tracks.
| Param | Type | Description |
| --- | --- | --- |
-| path | String
| Path the page/path to record a page view for |
-| extraProperties | Object
| Extra event properties to include in the event |
+| path | `String` | Path the page/path to record a page view for |
+| extraProperties | `Object` | Extra event properties to include in the event |
-# Debugging
+### bumpStat( statName, statValue )
+
+Bump a stat or group of stats.
+
+```typescript
+import { bumpStat } from '@woocommerce/tracks';
+
+// Bump a single stat
+bumpStat( 'stat_name', 'stat_value' );
+
+// Bump multiple stats
+bumpStat( {
+ stat1: 'value1',
+ stat2: 'value2'
+} );
+```
+
+| Param | Type | Description |
+| --- | --- | --- |
+| statName | `String` or `Object` | The name of the stat to bump, or an object of stat names and values |
+| statValue | `String` | The value for the stat (only used when statName is a string) |
+
+Note: Stat names are automatically prefixed with `x_woocommerce-`. Stat tracking is disabled in development mode.
+
+## Debugging
When debugging is activated info for each recorded Tracks event is logged to the browser console.
diff --git a/packages/js/tracks/changelog/add-bump-stats-error b/packages/js/tracks/changelog/add-bump-stats-error
new file mode 100644
index 00000000000..7649dba7346
--- /dev/null
+++ b/packages/js/tracks/changelog/add-bump-stats-error
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add bumpStats and fix unit test tooling
diff --git a/packages/js/tracks/jest.config.json b/packages/js/tracks/jest.config.json
new file mode 100644
index 00000000000..fa3347efcc7
--- /dev/null
+++ b/packages/js/tracks/jest.config.json
@@ -0,0 +1,7 @@
+{
+ "rootDir": "./",
+ "roots": [
+ "/src"
+ ],
+ "preset": "./node_modules/@woocommerce/internal-js-tests/jest-preset.js"
+}
diff --git a/packages/js/tracks/package.json b/packages/js/tracks/package.json
index 2cee918331c..82aaf9e44ee 100644
--- a/packages/js/tracks/package.json
+++ b/packages/js/tracks/package.json
@@ -47,6 +47,7 @@
"lint:fix:lang:js": "eslint src --fix",
"lint:lang:js": "eslint src",
"prepack": "pnpm build",
+ "test:js": "jest --config ./jest.config.json --passWithNoTests",
"watch:build": "pnpm --if-present --workspace-concurrency=Infinity --filter=\"$npm_package_name...\" --parallel '/^watch:build:project:.*$/'",
"watch:build:project": "pnpm --if-present run '/^watch:build:project:.*$/'",
"watch:build:project:cjs": "wireit",
@@ -55,7 +56,10 @@
"devDependencies": {
"@babel/core": "^7.23.5",
"@types/debug": "^4.1.12",
+ "@types/node": "^16.18.68",
+ "@types/jest": "^27.5.2",
"@woocommerce/eslint-plugin": "workspace:*",
+ "@woocommerce/internal-js-tests": "workspace:*",
"concurrently": "^7.6.0",
"eslint": "^8.55.0",
"jest": "~27.5.1",
@@ -121,6 +125,9 @@
"dependencyOutputs": {
"allowUsuallyExcludedPaths": true,
"files": [
+ "node_modules/@woocommerce/internal-js-tests/build",
+ "node_modules/@woocommerce/internal-js-tests/build-module",
+ "node_modules/@woocommerce/internal-js-tests/jest-preset.js",
"node_modules/@woocommerce/eslint-plugin/configs",
"node_modules/@woocommerce/eslint-plugin/rules",
"node_modules/@woocommerce/eslint-plugin/index.js",
diff --git a/packages/js/tracks/src/index.ts b/packages/js/tracks/src/index.ts
index e1cbe129026..b3acf81e00d 100644
--- a/packages/js/tracks/src/index.ts
+++ b/packages/js/tracks/src/index.ts
@@ -7,6 +7,7 @@ import debug from 'debug';
* Internal dependencies
*/
import { isDevelopmentMode } from './utils';
+export { bumpStat } from './stats';
/**
* Module variables
diff --git a/packages/js/tracks/src/stats.ts b/packages/js/tracks/src/stats.ts
new file mode 100644
index 00000000000..79a28497776
--- /dev/null
+++ b/packages/js/tracks/src/stats.ts
@@ -0,0 +1,87 @@
+/**
+ * External dependencies
+ */
+import debug from 'debug';
+
+/**
+ * Internal dependencies
+ */
+import { isDevelopmentMode } from './utils';
+
+/**
+ * Module variables
+ */
+const tracksDebug = debug( 'wc-admin:tracks:stats' );
+const GROUP_PREFIX = 'x_woocommerce-';
+
+/**
+ * Builds a query parameters from the given group and name parameters.
+ *
+ * This will automatically add the prefix `x_woocommerce-` to the group name.
+ *
+ * @param {Record | string} group - The group of stats or a single stat name.
+ * @param {string} [name] - The name of the stat if group is a string.
+ *
+ * @return {URLSearchParams} The constructed querys.
+ */
+function buildQueryParams(
+ group: Record< string, string > | string,
+ name: string
+): URLSearchParams {
+ const params = new URLSearchParams();
+ params.append( 'v', 'wpcom-no-pv' );
+
+ if ( typeof group !== 'object' ) {
+ params.append( `${ GROUP_PREFIX }${ group }`, name );
+ } else {
+ Object.entries( group as Record< string, string > ).forEach(
+ ( [ key, value ] ) => {
+ params.append( `${ GROUP_PREFIX }${ key }`, value );
+ }
+ );
+ }
+
+ // Add a random number to the query string to avoid caching.
+ params.append( 't', Math.random().toString() );
+
+ return params;
+}
+
+/**
+ * Bumps a stat or group of stats.
+ *
+ * @param {Record | string} group - The group of stats or a single stat name.
+ * @param {string} [name] - The name of the stat if group is a string.
+ * @return {boolean} True if the stat was successfully bumped, false otherwise.
+ */
+export function bumpStat(
+ group: Record< string, string > | string,
+ name = ''
+): boolean {
+ if ( typeof group === 'object' ) {
+ tracksDebug( 'Bumping stats %o', group );
+ } else {
+ tracksDebug( 'Bumping stat %s:%s', group, name );
+
+ if ( ! name ) {
+ tracksDebug( 'No stat name provided for group %s', group );
+ return false;
+ }
+ }
+
+ const shouldBumpStat =
+ ! isDevelopmentMode &&
+ !! window.wcTracks &&
+ !! window.wcTracks.isEnabled;
+
+ if ( ! shouldBumpStat ) {
+ return false;
+ }
+
+ const params = buildQueryParams( group, name );
+ new window.Image().src = `${
+ document.location.protocol
+ }//pixel.wp.com/g.gif?${ params.toString() }`;
+
+ return true;
+}
diff --git a/packages/js/tracks/src/test/stats.ts b/packages/js/tracks/src/test/stats.ts
new file mode 100644
index 00000000000..e4cecde9b80
--- /dev/null
+++ b/packages/js/tracks/src/test/stats.ts
@@ -0,0 +1,82 @@
+/**
+ * Internal dependencies
+ */
+import { bumpStat } from '../stats';
+
+jest.mock( '../utils', () => ( {
+ isDevelopmentMode: false,
+} ) );
+
+declare global {
+ interface Window {
+ Image: typeof Image;
+ }
+}
+
+describe( 'bumpStat', () => {
+ let originalImage: typeof Image;
+ let mockImage: { src: string };
+
+ beforeEach( () => {
+ originalImage = window.Image;
+ mockImage = { src: '' };
+ window.Image = jest.fn( () => mockImage ) as unknown as typeof Image;
+ window.wcTracks = {
+ isEnabled: true,
+ validateEvent: jest.fn(),
+ recordEvent: jest.fn(),
+ };
+ } );
+
+ afterEach( () => {
+ window.Image = originalImage;
+ jest.resetAllMocks();
+ } );
+
+ it( 'should not bump stats when wcTracks is not enabled', () => {
+ window.wcTracks.isEnabled = false;
+ const result = bumpStat( 'group', 'name' );
+ expect( result ).toBe( false );
+ expect( window.Image ).not.toHaveBeenCalled();
+ } );
+
+ it( 'should not bump stats in development mode', () => {
+ jest.resetModules();
+ jest.doMock( '../utils', () => ( {
+ isDevelopmentMode: true,
+ } ) );
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const { bumpStat: bumpStatDev } = require( '../stats' );
+
+ const result = bumpStatDev( 'group', 'name' );
+ expect( result ).toBe( false );
+ expect( window.Image ).not.toHaveBeenCalled();
+ } );
+
+ it( 'should not bump stats when name is empty given group is a string', () => {
+ const result = bumpStat( 'group', '' );
+
+ expect( result ).toBe( false );
+ expect( window.Image ).not.toHaveBeenCalled();
+ } );
+
+ it( 'should bump a single stat', () => {
+ const result = bumpStat( 'group', 'name' );
+
+ expect( result ).toBe( true );
+ expect( window.Image ).toHaveBeenCalledTimes( 1 );
+ expect( mockImage.src ).toMatch(
+ /^https?:\/\/pixel\.wp\.com\/g\.gif\?v=wpcom-no-pv&x_woocommerce-group=name&t=/
+ );
+ } );
+
+ it( 'should bump multiple stats', () => {
+ const result = bumpStat( { stat1: 'value1', stat2: 'value2' } );
+
+ expect( result ).toBe( true );
+ expect( window.Image ).toHaveBeenCalledTimes( 1 );
+ expect( mockImage.src ).toMatch(
+ /^https?:\/\/pixel\.wp\.com\/g\.gif\?v=wpcom-no-pv&x_woocommerce-stat1=value1&x_woocommerce-stat2=value2&t=/
+ );
+ } );
+} );
diff --git a/plugins/woocommerce-admin/client/error-boundary/index.tsx b/plugins/woocommerce-admin/client/error-boundary/index.tsx
index 1a36519730c..87e673462e5 100644
--- a/plugins/woocommerce-admin/client/error-boundary/index.tsx
+++ b/plugins/woocommerce-admin/client/error-boundary/index.tsx
@@ -5,6 +5,7 @@ import { Component, ReactNode, ErrorInfo } from 'react';
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { captureException } from '@woocommerce/remote-logging';
+import { bumpStat } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
@@ -38,6 +39,8 @@ export class ErrorBoundary extends Component<
componentDidCatch( error: Error, errorInfo: ErrorInfo ) {
this.setState( { errorInfo } );
+ bumpStat( 'error', 'unhandled-js-error-during-render' );
+
// Limit the component stack to 10 calls so we don't send too much data.
const componentStack = errorInfo.componentStack
.trim()
diff --git a/plugins/woocommerce/changelog/add-bump-stats-error b/plugins/woocommerce/changelog/add-bump-stats-error
new file mode 100644
index 00000000000..2a41a632378
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-bump-stats-error
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Track frequency of unhandled JS errors with MC Stats
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 562f27b24d3..2f72e783c85 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2824,6 +2824,9 @@ importers:
packages/js/remote-logging:
dependencies:
+ '@woocommerce/tracks':
+ specifier: workspace:*
+ version: link:../tracks
'@wordpress/hooks':
specifier: wp-6.0
version: 3.6.1
@@ -2892,9 +2895,18 @@ importers:
'@types/debug':
specifier: ^4.1.12
version: 4.1.12
+ '@types/jest':
+ specifier: ^27.5.2
+ version: 27.5.2
+ '@types/node':
+ specifier: ^16.18.68
+ version: 16.18.68
'@woocommerce/eslint-plugin':
specifier: workspace:*
version: link:../eslint-plugin
+ '@woocommerce/internal-js-tests':
+ specifier: workspace:*
+ version: link:../internal-js-tests
concurrently:
specifier: ^7.6.0
version: 7.6.0