From e8dacef7a609962eb25d15f9aed428819a8d74c9 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 2 Aug 2024 11:04:31 +0800 Subject: [PATCH] Track frequency of unhandled JS errors with MC Stats (#50155) * Add bumpStat and fix tests * Add changelog * chore: Update dependencies and add @woocommerce/tracks for remote logging * feat: Track frequency of unhandled JS errors with bumpStat * chore: Update error boundary to log unhandled JS errors with bumpStat * Add changelog * Fix lint * Check if tracks is enabled before bumping stats * Fix test * Fix lint * chore: Refactor buildQuerystring to buildQueryParams for clarity and consistency * Add bumpStat to wc tracks mock --- .../changelog/add-bump-stats-error | 4 + packages/js/internal-js-tests/composer.lock | 1059 +++++++++++++++++ .../src/mocks/woocommerce-tracks.js | 1 + .../changelog/add-bump-stats-error | 4 + packages/js/remote-logging/package.json | 4 + .../js/remote-logging/src/remote-logger.ts | 4 + packages/js/tracks/README.md | 41 +- .../js/tracks/changelog/add-bump-stats-error | 4 + packages/js/tracks/jest.config.json | 7 + packages/js/tracks/package.json | 7 + packages/js/tracks/src/index.ts | 1 + packages/js/tracks/src/stats.ts | 87 ++ packages/js/tracks/src/test/stats.ts | 82 ++ .../client/error-boundary/index.tsx | 3 + .../changelog/add-bump-stats-error | 4 + pnpm-lock.yaml | 12 + 16 files changed, 1315 insertions(+), 9 deletions(-) create mode 100644 packages/js/internal-js-tests/changelog/add-bump-stats-error create mode 100644 packages/js/internal-js-tests/composer.lock create mode 100644 packages/js/remote-logging/changelog/add-bump-stats-error create mode 100644 packages/js/tracks/changelog/add-bump-stats-error create mode 100644 packages/js/tracks/jest.config.json create mode 100644 packages/js/tracks/src/stats.ts create mode 100644 packages/js/tracks/src/test/stats.ts create mode 100644 plugins/woocommerce/changelog/add-bump-stats-error 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