From 5cac08d48a4380e18f35637b21c36adcd05f35c7 Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Mon, 5 Jun 2023 15:17:21 -0700 Subject: [PATCH 001/214] build: allow lerna version commands from v1.x branch --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index fbc25b49f..5328648e6 100644 --- a/lerna.json +++ b/lerna.json @@ -7,7 +7,7 @@ "useWorkspaces": true, "command": { "version": { - "allowBranch": "main", + "allowBranch": "v1.x", "conventionalCommits": true, "createRelease": "github", "message": "chore(release): publish", From cd8e6bd13a779937a61d32dd30439c89aeee4ab2 Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Mon, 5 Jun 2023 16:27:28 -0700 Subject: [PATCH 002/214] build: support release jobs for multiple versions (#403) --- .github/workflows/ci.yml | 2 + .github/workflows/publish-v1.yml | 117 +++++++++++++++++++ .github/workflows/{cd.yml => publish-v2.yml} | 14 +-- .github/workflows/s3-upload-test.yml | 43 ------- 4 files changed, 126 insertions(+), 50 deletions(-) create mode 100644 .github/workflows/publish-v1.yml rename .github/workflows/{cd.yml => publish-v2.yml} (92%) delete mode 100644 .github/workflows/s3-upload-test.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a67cd2ff..371afb256 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,11 @@ on: push: branches: - main + - v1.x pull_request: branches: - main + - v1.x jobs: build: diff --git a/.github/workflows/publish-v1.yml b/.github/workflows/publish-v1.yml new file mode 100644 index 000000000..da4e5e204 --- /dev/null +++ b/.github/workflows/publish-v1.yml @@ -0,0 +1,117 @@ +name: Publish v1.x + +on: + workflow_dispatch: + inputs: + releaseType: + type: choice + description: Release Type + options: + - release + - prerelease + - graduate + +jobs: + authorize: + name: Authorize + runs-on: ubuntu-latest + steps: + - name: ${{ github.actor }} permission check to do a release + uses: 'lannonbr/repo-permission-check-action@2.0.2' + with: + permission: 'write' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + deploy: + name: Deploy + runs-on: ubuntu-latest + needs: [authorize] + permissions: + id-token: write + contents: write + env: + RELEASE_TYPE: ${{ github.event.inputs.releaseType }} + strategy: + matrix: + node-version: [16.x] + + steps: + - name: Check out git repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: v1.x + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: '**/node_modules' + key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install project dependencies + run: | + yarn install --frozen-lockfile + + - name: Build all packages + run: | + yarn build + + - name: Test all packages + run: | + yarn test + + - name: Lint all packages + run: | + yarn lint + + - name: Configure Git User + run: | + git config --global user.name amplitude-sdk-bot + git config --global user.email amplitude-sdk-bot@users.noreply.github.com + + - name: Configure NPM User + run: | + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_PUBLISH_TOKEN }}" > ~/.npmrc + npm whoami + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + role-to-assume: arn:aws:iam::358203115967:role/github-actions-role + aws-region: us-west-2 + + # https://www.npmjs.com/package/@lerna/version#--conventional-prerelease + # patch: 1.0.0 -> 1.0.1-alpha.0 + # minor: 1.0.0 -> 1.1.0-alpha.0 + # major: 1.0.0 -> 2.0.0-alpha.0 + - name: Create pre-release version + if: ${{ env.RELEASE_TYPE == 'prerelease'}} + run: | + GH_TOKEN=${{ secrets.GH_PUBLISH_TOKEN }} npm run deploy:version -- -y --conventional-prerelease + + # https://www.npmjs.com/package/@lerna/version#--conventional-graduate + # 1.0.0-alpha.0 -> 1.0.1 + - name: Create graduate version + if: ${{ env.RELEASE_TYPE == 'graduate'}} + run: | + GH_TOKEN=${{ secrets.GH_PUBLISH_TOKEN }} npm run deploy:version -- -y --conventional-graduate + + # Use 'release' for the usual deployment + # NOTE: You probably want this + - name: Create release version + if: ${{ env.RELEASE_TYPE == 'release'}} + run: | + GH_TOKEN=${{ secrets.GH_PUBLISH_TOKEN }} npm run deploy:version -- -y + + # Use 'from git' option if `lerna version` has already been run + - name: Publish Release to NPM + run: | + GH_TOKEN=${{ secrets.GH_PUBLISH_TOKEN }} npm run deploy:publish -- from-git -y + env: + S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }} diff --git a/.github/workflows/cd.yml b/.github/workflows/publish-v2.yml similarity index 92% rename from .github/workflows/cd.yml rename to .github/workflows/publish-v2.yml index 2a4f9f201..1e5006065 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/publish-v2.yml @@ -1,4 +1,4 @@ -name: Continuous Deployment +name: Publish v2.x on: workflow_dispatch: @@ -16,12 +16,12 @@ jobs: name: Authorize runs-on: ubuntu-latest steps: - - name: ${{ github.actor }} permission check to do a release - uses: "lannonbr/repo-permission-check-action@2.0.2" - with: - permission: "write" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: ${{ github.actor }} permission check to do a release + uses: 'lannonbr/repo-permission-check-action@2.0.2' + with: + permission: 'write' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} deploy: name: Deploy diff --git a/.github/workflows/s3-upload-test.yml b/.github/workflows/s3-upload-test.yml deleted file mode 100644 index 635b24a95..000000000 --- a/.github/workflows/s3-upload-test.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: S3 Upload Test - -on: workflow_dispatch - -jobs: - deploy: - name: Deploy - runs-on: ubuntu-latest - permissions: - id-token: write - contents: read - strategy: - matrix: - node-version: [16.x] - - steps: - - name: Check out git repository - uses: actions/checkout@v3 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - - name: Install project dependencies - run: | - yarn install --frozen-lockfile - - - name: Build all packages - run: | - yarn build - - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - role-to-assume: arn:aws:iam::358203115967:role/github-actions-role - aws-region: us-west-2 - - - name: Publish to S3 - run: | - cd packages/analytics-browser && node scripts/publish/upload-to-s3.js - env: - S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }} From 05eb25a635c91ee2f11d2391483678dd0768f80b Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Tue, 6 Jun 2023 00:04:29 +0000 Subject: [PATCH 003/214] chore(release): publish - @amplitude/analytics-browser-test@1.4.0-beta.1 - @amplitude/analytics-browser@1.10.6-beta.0 - @amplitude/analytics-client-common@1.0.0-beta.1 - @amplitude/analytics-core@1.0.0-beta.1 - @amplitude/analytics-node-test@1.0.0-beta.1 - @amplitude/analytics-node@1.1.7-beta.0 - @amplitude/analytics-react-native@1.1.8-beta.0 - @amplitude/analytics-types@1.0.0-beta.1 - @amplitude/marketing-analytics-browser@1.0.0-beta.1 - @amplitude/plugin-page-view-tracking-browser@1.0.0-beta.1 - @amplitude/plugin-web-attribution-browser@1.0.0-beta.1 --- packages/analytics-browser-test/CHANGELOG.md | 9 +++++++++ packages/analytics-browser-test/package.json | 4 ++-- packages/analytics-browser/CHANGELOG.md | 9 +++++++++ packages/analytics-browser/package.json | 12 ++++++------ packages/analytics-browser/playground/amplitude.js | 2 +- packages/analytics-browser/src/version.ts | 2 +- packages/analytics-client-common/CHANGELOG.md | 9 +++++++++ packages/analytics-client-common/package.json | 6 +++--- packages/analytics-core/CHANGELOG.md | 9 +++++++++ packages/analytics-core/package.json | 4 ++-- packages/analytics-node-test/CHANGELOG.md | 9 +++++++++ packages/analytics-node-test/package.json | 4 ++-- packages/analytics-node/CHANGELOG.md | 9 +++++++++ packages/analytics-node/package.json | 6 +++--- packages/analytics-node/src/version.ts | 2 +- packages/analytics-react-native/CHANGELOG.md | 9 +++++++++ packages/analytics-react-native/package.json | 8 ++++---- packages/analytics-react-native/src/version.ts | 2 +- packages/analytics-types/CHANGELOG.md | 9 +++++++++ packages/analytics-types/package.json | 2 +- packages/marketing-analytics-browser/CHANGELOG.md | 9 +++++++++ packages/marketing-analytics-browser/package.json | 14 +++++++------- .../marketing-analytics-browser/src/version.ts | 2 +- .../plugin-page-view-tracking-browser/CHANGELOG.md | 9 +++++++++ .../plugin-page-view-tracking-browser/package.json | 8 ++++---- .../plugin-web-attribution-browser/CHANGELOG.md | 9 +++++++++ .../plugin-web-attribution-browser/package.json | 8 ++++---- 27 files changed, 142 insertions(+), 43 deletions(-) diff --git a/packages/analytics-browser-test/CHANGELOG.md b/packages/analytics-browser-test/CHANGELOG.md index 04d7e39ea..28f8c7b48 100644 --- a/packages/analytics-browser-test/CHANGELOG.md +++ b/packages/analytics-browser-test/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.4.0-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@2.7.0...@amplitude/analytics-browser-test@1.4.0-beta.1) (2023-06-06) + +**Note:** Version bump only for package @amplitude/analytics-browser-test + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [2.7.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@2.6.2...@amplitude/analytics-browser-test@2.7.0) (2023-05-04) ### Features diff --git a/packages/analytics-browser-test/package.json b/packages/analytics-browser-test/package.json index e6a5f1ec8..cca8aa9f4 100644 --- a/packages/analytics-browser-test/package.json +++ b/packages/analytics-browser-test/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser-test", - "version": "1.4.0-beta.0", + "version": "1.4.0-beta.1", "private": true, "description": "", "author": "Amplitude Inc", @@ -16,7 +16,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.10.5" + "@amplitude/analytics-browser": "^1.10.6-beta.0" }, "devDependencies": { "nock": "^13.2.4" diff --git a/packages/analytics-browser/CHANGELOG.md b/packages/analytics-browser/CHANGELOG.md index 70d676c63..34ee26082 100644 --- a/packages/analytics-browser/CHANGELOG.md +++ b/packages/analytics-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.10.6-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.10.5...@amplitude/analytics-browser@1.10.6-beta.0) (2023-06-06) + +**Note:** Version bump only for package @amplitude/analytics-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.10.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.10.4...@amplitude/analytics-browser@1.10.5) (2023-06-05) **Note:** Version bump only for package @amplitude/analytics-browser diff --git a/packages/analytics-browser/package.json b/packages/analytics-browser/package.json index 5e075ba18..5d769470d 100644 --- a/packages/analytics-browser/package.json +++ b/packages/analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser", - "version": "1.10.5", + "version": "1.10.6-beta.0", "description": "Official Amplitude SDK for Web", "keywords": [ "analytics", @@ -43,11 +43,11 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.0-beta.0", - "@amplitude/analytics-core": "^1.0.0-beta.0", - "@amplitude/analytics-types": "^1.0.0-beta.0", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.0-beta.0", - "@amplitude/plugin-web-attribution-browser": "^1.0.0-beta.0", + "@amplitude/analytics-client-common": "^1.0.0-beta.1", + "@amplitude/analytics-core": "^1.0.0-beta.1", + "@amplitude/analytics-types": "^1.0.0-beta.1", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.0-beta.1", + "@amplitude/plugin-web-attribution-browser": "^1.0.0-beta.1", "@amplitude/ua-parser-js": "^0.7.31", "tslib": "^2.4.1" }, diff --git a/packages/analytics-browser/playground/amplitude.js b/packages/analytics-browser/playground/amplitude.js index 95196138e..1540df58e 100644 --- a/packages/analytics-browser/playground/amplitude.js +++ b/packages/analytics-browser/playground/amplitude.js @@ -1 +1 @@ -!function(){"use strict";var e=function(t,i){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])},e(t,i)};function t(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=t}e(t,i),t.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var i=function(){return i=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!V(t,i))return!1}return!0},V=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&M(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return M(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},z=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||V(t,i)))},e}(),F=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},B=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},$=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return B(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),Q="Event rejected due to exceeded retry count",K=function(e){return{promise:e||Promise.resolve()}},H=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new $(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return K(this.dispatch(r))},e.prototype.identify=function(e,t){var i=F(e,t);return K(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return K(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new z;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return K(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return K(this.dispatch(n))},e.prototype.add=function(e){return this.config?K(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),K())},e.prototype.remove=function(e){return this.config?K(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),K())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(B(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,B(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=B(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return K(this.timeline.flush())},e}(),W=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return M(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),Z="Amplitude Logger ",G=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=ee(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:if(null===(s=o.sent()))return this.fulfillRequest(e,0,"Unexpected error occurred"),[2];if(!t){if("body"in s){a="";try{a=JSON.stringify(s.body,null,2)}catch(e){}this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(a))}else this.fulfillRequest(e,s.statusCode,s.status);return[2]}return this.handleReponse(s,e),[3,4];case 3:return u=o.sent(),this.fulfillRequest(e,0,String(u)),[3,4];case 4:return[2]}}))}))},e.prototype.handleReponse=function(e,t){switch(e.status){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.handleOtherReponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherReponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(B(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ie=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},ne=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},re=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},oe=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=re(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},se=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ae)},ue=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),ce=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),le=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[C,t,e.substring(0,i)].filter(Boolean).join("_")},de=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),pe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(ce),fe="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},ve={exports:{}};q=ve,A=ve.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",D="Xiaomi",C="Zebra",j="Facebook",L=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},B=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?z(e,275):e,this},this.setUA(f),this};K.VERSION="0.7.31",K.BROWSER=L([a,l,"major"]),K.CPU=L([d]),K.DEVICE=L([s,c,u,p,f,h,v,g,b]),K.ENGINE=K.OS=L([a,l]),q.exports&&(A=q.exports=K),A.UAParser=K;var H=typeof e!==n&&(e.jQuery||e.Zepto);if(H&&!H.ua){var W=new K;H.ua=W.getResult(),H.ua.get=function(){return W.getUA()},H.ua.set=function(e){W.setUA(e);var t=W.getResult();for(var i in t)H.ua[i]=t[i]}}}("object"==typeof window?window:fe);var he=ve.exports,ge=function(){function e(){this.ua=new ve.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:me(),platform:"Web",os:be(this.ua),deviceModel:ye(this.ua)}},e}(),be=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},ye=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},me=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},we=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),_e=function(){return _e=Object.assign||function(e){for(var t,i=1,n=arguments.length;ie.sessionTimeout&&(t.setSessionId(n),i.session_id=t.getSessionId(),i.time=n),e.lastEventTime=n,[2,i])}))}))}}},bt={page_domain:"".concat(tt," Page Domain"),page_location:"".concat(tt," Page Location"),page_path:"".concat(tt," Page Path"),page_title:"".concat(tt," Page Title"),page_url:"".concat(tt," Page URL")},yt=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.init=function(e,t,n){return void 0===e&&(e=""),K(this._init(i(i({},n),{userId:t,apiKey:e})))},n.prototype._init=function(t){var n,s,u,c,l,d,p,v,h,g,b,m,w,_,I,k,E,S;return r(this,void 0,void 0,(function(){var T,O,x,P,R,N,q,A,D,C,j,L,M,V,z,F,B,$,Q=this;return o(this,(function(K){switch(K.label){case 0:return this.initializing?[2]:(this.initializing=!0,T=t,t.disableCookies?(O="",[3,5]):[3,1]);case 1:return null===(n=t.domain)||void 0===n?[3,2]:(x=n,[3,4]);case 2:return[4,r(void 0,void 0,void 0,(function(){var e,t,i,n,r,s,a;return o(this,(function(o){switch(o.label){case 0:return[4,(new de).isEnabled()];case 1:if(!o.sent()||!W&&"undefined"==typeof location)return[2,""];for(e=null!=W?W:location.hostname,t=e.split("."),i=[],n="AMP_TLDTEST",r=t.length-2;r>=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(Date.now()),z=!0),(F=Te()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new te).promise];case 11:return K.sent(),[4,this.add(new Ue).promise];case 12:return K.sent(),[4,this.add(gt()).promise];case 13:return K.sent(),[4,this.add(new Oe).promise];case 14:return K.sent(),("boolean"==typeof(H=this.config.defaultTracking)?H:null==H?void 0:H.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(ot,((n={})[ut]=o,n[ct]=i.pathname,n[lt]=e.id,n[dt]=e.text,n[pt]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,16];case 15:K.sent(),K.label=16;case 16:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),t.track(rt,((r={})[ft]=e.id,r[vt]=e.name,r[ht]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,18];case 17:K.sent(),K.label=18;case 18:return(null===(_=this.config.attribution)||void 0===_?void 0:_.disabled)?[3,20]:(B=function(){for(var e,t,n=this,s=[],u=0;u0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!V(t,i))return!1}return!0},V=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&M(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return M(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},z=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||V(t,i)))},e}(),F=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},B=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},$=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return B(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),Q="Event rejected due to exceeded retry count",K=function(e){return{promise:e||Promise.resolve()}},H=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new $(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return K(this.dispatch(r))},e.prototype.identify=function(e,t){var i=F(e,t);return K(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return K(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new z;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return K(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return K(this.dispatch(n))},e.prototype.add=function(e){return this.config?K(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),K())},e.prototype.remove=function(e){return this.config?K(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),K())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(B(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,B(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=B(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return K(this.timeline.flush())},e}(),W=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return M(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),Z="Amplitude Logger ",G=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=ee(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:if(null===(s=o.sent()))return this.fulfillRequest(e,0,"Unexpected error occurred"),[2];if(!t){if("body"in s){a="";try{a=JSON.stringify(s.body,null,2)}catch(e){}this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(a))}else this.fulfillRequest(e,s.statusCode,s.status);return[2]}return this.handleReponse(s,e),[3,4];case 3:return u=o.sent(),this.fulfillRequest(e,0,String(u)),[3,4];case 4:return[2]}}))}))},e.prototype.handleReponse=function(e,t){switch(e.status){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.handleOtherReponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherReponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(B(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ie=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},ne=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},re=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},oe=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=re(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},se=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ae)},ue=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),ce=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),le=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[C,t,e.substring(0,i)].filter(Boolean).join("_")},de=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),pe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(ce),fe="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},ve={exports:{}};q=ve,A=ve.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",D="Xiaomi",C="Zebra",j="Facebook",L=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},B=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?z(e,275):e,this},this.setUA(f),this};K.VERSION="0.7.31",K.BROWSER=L([a,l,"major"]),K.CPU=L([d]),K.DEVICE=L([s,c,u,p,f,h,v,g,b]),K.ENGINE=K.OS=L([a,l]),q.exports&&(A=q.exports=K),A.UAParser=K;var H=typeof e!==n&&(e.jQuery||e.Zepto);if(H&&!H.ua){var W=new K;H.ua=W.getResult(),H.ua.get=function(){return W.getUA()},H.ua.set=function(e){W.setUA(e);var t=W.getResult();for(var i in t)H.ua[i]=t[i]}}}("object"==typeof window?window:fe);var he=ve.exports,ge=function(){function e(){this.ua=new ve.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:me(),platform:"Web",os:be(this.ua),deviceModel:ye(this.ua)}},e}(),be=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},ye=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},me=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},we=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),_e=function(){return _e=Object.assign||function(e){for(var t,i=1,n=arguments.length;ie.sessionTimeout&&(t.setSessionId(n),i.session_id=t.getSessionId(),i.time=n),e.lastEventTime=n,[2,i])}))}))}}},bt={page_domain:"".concat(tt," Page Domain"),page_location:"".concat(tt," Page Location"),page_path:"".concat(tt," Page Path"),page_title:"".concat(tt," Page Title"),page_url:"".concat(tt," Page URL")},yt=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.init=function(e,t,n){return void 0===e&&(e=""),K(this._init(i(i({},n),{userId:t,apiKey:e})))},n.prototype._init=function(t){var n,s,u,c,l,d,p,v,h,g,b,m,w,_,I,k,E,S;return r(this,void 0,void 0,(function(){var T,O,x,P,R,N,q,A,D,C,j,L,M,V,z,F,B,$,Q=this;return o(this,(function(K){switch(K.label){case 0:return this.initializing?[2]:(this.initializing=!0,T=t,t.disableCookies?(O="",[3,5]):[3,1]);case 1:return null===(n=t.domain)||void 0===n?[3,2]:(x=n,[3,4]);case 2:return[4,r(void 0,void 0,void 0,(function(){var e,t,i,n,r,s,a;return o(this,(function(o){switch(o.label){case 0:return[4,(new de).isEnabled()];case 1:if(!o.sent()||!W&&"undefined"==typeof location)return[2,""];for(e=null!=W?W:location.hostname,t=e.split("."),i=[],n="AMP_TLDTEST",r=t.length-2;r>=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(Date.now()),z=!0),(F=Te()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new te).promise];case 11:return K.sent(),[4,this.add(new Ue).promise];case 12:return K.sent(),[4,this.add(gt()).promise];case 13:return K.sent(),[4,this.add(new Oe).promise];case 14:return K.sent(),("boolean"==typeof(H=this.config.defaultTracking)?H:null==H?void 0:H.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(ot,((n={})[ut]=o,n[ct]=i.pathname,n[lt]=e.id,n[dt]=e.text,n[pt]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,16];case 15:K.sent(),K.label=16;case 16:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),t.track(rt,((r={})[ft]=e.id,r[vt]=e.name,r[ht]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,18];case 17:K.sent(),K.label=18;case 18:return(null===(_=this.config.attribution)||void 0===_?void 0:_.disabled)?[3,20]:(B=function(){for(var e,t,n=this,s=[],u=0;u Date: Tue, 6 Jun 2023 00:49:36 +0000 Subject: [PATCH 004/214] chore(release): publish - @amplitude/analytics-browser-test@1.4.0 - @amplitude/analytics-browser@1.10.6 - @amplitude/analytics-client-common@1.0.0 - @amplitude/analytics-core@1.0.0 - @amplitude/analytics-node-test@1.0.0 - @amplitude/analytics-node@1.1.7 - @amplitude/analytics-react-native@1.1.8 - @amplitude/analytics-types@1.0.0 - @amplitude/marketing-analytics-browser@1.0.0 - @amplitude/plugin-page-view-tracking-browser@1.0.0 - @amplitude/plugin-web-attribution-browser@1.0.0 --- packages/analytics-browser-test/CHANGELOG.md | 9 +++++++++ packages/analytics-browser-test/package.json | 4 ++-- packages/analytics-browser/CHANGELOG.md | 9 +++++++++ packages/analytics-browser/README.md | 2 +- .../generated/amplitude-snippet.js | 4 ++-- packages/analytics-browser/package.json | 12 ++++++------ packages/analytics-browser/playground/amplitude.js | 2 +- packages/analytics-browser/src/version.ts | 2 +- packages/analytics-client-common/CHANGELOG.md | 9 +++++++++ packages/analytics-client-common/package.json | 6 +++--- packages/analytics-core/CHANGELOG.md | 9 +++++++++ packages/analytics-core/package.json | 4 ++-- packages/analytics-node-test/CHANGELOG.md | 9 +++++++++ packages/analytics-node-test/package.json | 4 ++-- packages/analytics-node/CHANGELOG.md | 9 +++++++++ packages/analytics-node/package.json | 6 +++--- packages/analytics-node/src/version.ts | 2 +- packages/analytics-react-native/CHANGELOG.md | 9 +++++++++ packages/analytics-react-native/package.json | 8 ++++---- packages/analytics-react-native/src/version.ts | 2 +- packages/analytics-types/CHANGELOG.md | 9 +++++++++ packages/analytics-types/package.json | 2 +- packages/marketing-analytics-browser/CHANGELOG.md | 9 +++++++++ packages/marketing-analytics-browser/README.md | 2 +- .../generated/amplitude-gtm-snippet.js | 4 ++-- .../generated/amplitude-snippet.js | 4 ++-- packages/marketing-analytics-browser/package.json | 14 +++++++------- .../marketing-analytics-browser/src/version.ts | 2 +- .../plugin-page-view-tracking-browser/CHANGELOG.md | 9 +++++++++ .../plugin-page-view-tracking-browser/package.json | 8 ++++---- .../plugin-web-attribution-browser/CHANGELOG.md | 9 +++++++++ .../plugin-web-attribution-browser/package.json | 8 ++++---- 32 files changed, 150 insertions(+), 51 deletions(-) diff --git a/packages/analytics-browser-test/CHANGELOG.md b/packages/analytics-browser-test/CHANGELOG.md index 28f8c7b48..5a9c21546 100644 --- a/packages/analytics-browser-test/CHANGELOG.md +++ b/packages/analytics-browser-test/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.4.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.0-beta.1...@amplitude/analytics-browser-test@1.4.0) (2023-06-06) + +**Note:** Version bump only for package @amplitude/analytics-browser-test + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [1.4.0-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@2.7.0...@amplitude/analytics-browser-test@1.4.0-beta.1) (2023-06-06) **Note:** Version bump only for package @amplitude/analytics-browser-test diff --git a/packages/analytics-browser-test/package.json b/packages/analytics-browser-test/package.json index cca8aa9f4..86af55ecc 100644 --- a/packages/analytics-browser-test/package.json +++ b/packages/analytics-browser-test/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser-test", - "version": "1.4.0-beta.1", + "version": "1.4.0", "private": true, "description": "", "author": "Amplitude Inc", @@ -16,7 +16,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.10.6-beta.0" + "@amplitude/analytics-browser": "^1.10.6" }, "devDependencies": { "nock": "^13.2.4" diff --git a/packages/analytics-browser/CHANGELOG.md b/packages/analytics-browser/CHANGELOG.md index 34ee26082..9af42e906 100644 --- a/packages/analytics-browser/CHANGELOG.md +++ b/packages/analytics-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.10.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.10.6-beta.0...@amplitude/analytics-browser@1.10.6) (2023-06-06) + +**Note:** Version bump only for package @amplitude/analytics-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.10.6-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.10.5...@amplitude/analytics-browser@1.10.6-beta.0) (2023-06-06) **Note:** Version bump only for package @amplitude/analytics-browser diff --git a/packages/analytics-browser/README.md b/packages/analytics-browser/README.md index fb9a9ca12..ca05dd049 100644 --- a/packages/analytics-browser/README.md +++ b/packages/analytics-browser/README.md @@ -40,7 +40,7 @@ Alternatively, the package is also distributed through a CDN. Copy and paste the ```html diff --git a/packages/analytics-browser/generated/amplitude-snippet.js b/packages/analytics-browser/generated/amplitude-snippet.js index 6fe25dc9f..18820c265 100644 --- a/packages/analytics-browser/generated/amplitude-snippet.js +++ b/packages/analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-wZAYoU/Dyp5YYHRl09XkA/xsjCGm1JgxV6S6nBBmjPIU9BEGyQvTKUEXbjCUoZPJ'; + as.integrity = 'sha384-/pEyHhmhzhlIzPsIq1ptBvg2RLgkHo5wAhA7DEi6bJ+jNOQ7Us/bz8mfKQ8hGyZX'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.10.5-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.10.6-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/analytics-browser/package.json b/packages/analytics-browser/package.json index 5d769470d..6d3345b8e 100644 --- a/packages/analytics-browser/package.json +++ b/packages/analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser", - "version": "1.10.6-beta.0", + "version": "1.10.6", "description": "Official Amplitude SDK for Web", "keywords": [ "analytics", @@ -43,11 +43,11 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.0-beta.1", - "@amplitude/analytics-core": "^1.0.0-beta.1", - "@amplitude/analytics-types": "^1.0.0-beta.1", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.0-beta.1", - "@amplitude/plugin-web-attribution-browser": "^1.0.0-beta.1", + "@amplitude/analytics-client-common": "^1.0.0", + "@amplitude/analytics-core": "^1.0.0", + "@amplitude/analytics-types": "^1.0.0", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.0", + "@amplitude/plugin-web-attribution-browser": "^1.0.0", "@amplitude/ua-parser-js": "^0.7.31", "tslib": "^2.4.1" }, diff --git a/packages/analytics-browser/playground/amplitude.js b/packages/analytics-browser/playground/amplitude.js index 1540df58e..e0aa00876 100644 --- a/packages/analytics-browser/playground/amplitude.js +++ b/packages/analytics-browser/playground/amplitude.js @@ -1 +1 @@ -!function(){"use strict";var e=function(t,i){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])},e(t,i)};function t(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=t}e(t,i),t.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var i=function(){return i=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!V(t,i))return!1}return!0},V=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&M(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return M(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},z=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||V(t,i)))},e}(),F=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},B=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},$=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return B(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),Q="Event rejected due to exceeded retry count",K=function(e){return{promise:e||Promise.resolve()}},H=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new $(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return K(this.dispatch(r))},e.prototype.identify=function(e,t){var i=F(e,t);return K(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return K(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new z;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return K(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return K(this.dispatch(n))},e.prototype.add=function(e){return this.config?K(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),K())},e.prototype.remove=function(e){return this.config?K(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),K())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(B(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,B(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=B(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return K(this.timeline.flush())},e}(),W=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return M(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),Z="Amplitude Logger ",G=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=ee(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:if(null===(s=o.sent()))return this.fulfillRequest(e,0,"Unexpected error occurred"),[2];if(!t){if("body"in s){a="";try{a=JSON.stringify(s.body,null,2)}catch(e){}this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(a))}else this.fulfillRequest(e,s.statusCode,s.status);return[2]}return this.handleReponse(s,e),[3,4];case 3:return u=o.sent(),this.fulfillRequest(e,0,String(u)),[3,4];case 4:return[2]}}))}))},e.prototype.handleReponse=function(e,t){switch(e.status){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.handleOtherReponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherReponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(B(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ie=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},ne=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},re=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},oe=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=re(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},se=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ae)},ue=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),ce=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),le=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[C,t,e.substring(0,i)].filter(Boolean).join("_")},de=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),pe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(ce),fe="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},ve={exports:{}};q=ve,A=ve.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",D="Xiaomi",C="Zebra",j="Facebook",L=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},B=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?z(e,275):e,this},this.setUA(f),this};K.VERSION="0.7.31",K.BROWSER=L([a,l,"major"]),K.CPU=L([d]),K.DEVICE=L([s,c,u,p,f,h,v,g,b]),K.ENGINE=K.OS=L([a,l]),q.exports&&(A=q.exports=K),A.UAParser=K;var H=typeof e!==n&&(e.jQuery||e.Zepto);if(H&&!H.ua){var W=new K;H.ua=W.getResult(),H.ua.get=function(){return W.getUA()},H.ua.set=function(e){W.setUA(e);var t=W.getResult();for(var i in t)H.ua[i]=t[i]}}}("object"==typeof window?window:fe);var he=ve.exports,ge=function(){function e(){this.ua=new ve.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:me(),platform:"Web",os:be(this.ua),deviceModel:ye(this.ua)}},e}(),be=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},ye=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},me=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},we=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),_e=function(){return _e=Object.assign||function(e){for(var t,i=1,n=arguments.length;ie.sessionTimeout&&(t.setSessionId(n),i.session_id=t.getSessionId(),i.time=n),e.lastEventTime=n,[2,i])}))}))}}},bt={page_domain:"".concat(tt," Page Domain"),page_location:"".concat(tt," Page Location"),page_path:"".concat(tt," Page Path"),page_title:"".concat(tt," Page Title"),page_url:"".concat(tt," Page URL")},yt=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.init=function(e,t,n){return void 0===e&&(e=""),K(this._init(i(i({},n),{userId:t,apiKey:e})))},n.prototype._init=function(t){var n,s,u,c,l,d,p,v,h,g,b,m,w,_,I,k,E,S;return r(this,void 0,void 0,(function(){var T,O,x,P,R,N,q,A,D,C,j,L,M,V,z,F,B,$,Q=this;return o(this,(function(K){switch(K.label){case 0:return this.initializing?[2]:(this.initializing=!0,T=t,t.disableCookies?(O="",[3,5]):[3,1]);case 1:return null===(n=t.domain)||void 0===n?[3,2]:(x=n,[3,4]);case 2:return[4,r(void 0,void 0,void 0,(function(){var e,t,i,n,r,s,a;return o(this,(function(o){switch(o.label){case 0:return[4,(new de).isEnabled()];case 1:if(!o.sent()||!W&&"undefined"==typeof location)return[2,""];for(e=null!=W?W:location.hostname,t=e.split("."),i=[],n="AMP_TLDTEST",r=t.length-2;r>=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(Date.now()),z=!0),(F=Te()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new te).promise];case 11:return K.sent(),[4,this.add(new Ue).promise];case 12:return K.sent(),[4,this.add(gt()).promise];case 13:return K.sent(),[4,this.add(new Oe).promise];case 14:return K.sent(),("boolean"==typeof(H=this.config.defaultTracking)?H:null==H?void 0:H.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(ot,((n={})[ut]=o,n[ct]=i.pathname,n[lt]=e.id,n[dt]=e.text,n[pt]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,16];case 15:K.sent(),K.label=16;case 16:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),t.track(rt,((r={})[ft]=e.id,r[vt]=e.name,r[ht]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,18];case 17:K.sent(),K.label=18;case 18:return(null===(_=this.config.attribution)||void 0===_?void 0:_.disabled)?[3,20]:(B=function(){for(var e,t,n=this,s=[],u=0;u0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!V(t,i))return!1}return!0},V=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&M(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return M(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},z=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||V(t,i)))},e}(),F=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},B=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},$=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return B(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),Q="Event rejected due to exceeded retry count",K=function(e){return{promise:e||Promise.resolve()}},H=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new $(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return K(this.dispatch(r))},e.prototype.identify=function(e,t){var i=F(e,t);return K(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return K(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new z;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return K(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return K(this.dispatch(n))},e.prototype.add=function(e){return this.config?K(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),K())},e.prototype.remove=function(e){return this.config?K(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),K())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(B(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,B(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=B(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return K(this.timeline.flush())},e}(),W=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return M(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),Z="Amplitude Logger ",G=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=ee(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:if(null===(s=o.sent()))return this.fulfillRequest(e,0,"Unexpected error occurred"),[2];if(!t){if("body"in s){a="";try{a=JSON.stringify(s.body,null,2)}catch(e){}this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(a))}else this.fulfillRequest(e,s.statusCode,s.status);return[2]}return this.handleReponse(s,e),[3,4];case 3:return u=o.sent(),this.fulfillRequest(e,0,String(u)),[3,4];case 4:return[2]}}))}))},e.prototype.handleReponse=function(e,t){switch(e.status){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.handleOtherReponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherReponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(B(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ie=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},ne=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},re=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},oe=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=re(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},se=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ae)},ue=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),ce=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),le=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[C,t,e.substring(0,i)].filter(Boolean).join("_")},de=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),pe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(ce),fe="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},ve={exports:{}};q=ve,A=ve.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",D="Xiaomi",C="Zebra",j="Facebook",L=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},B=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?z(e,275):e,this},this.setUA(f),this};K.VERSION="0.7.31",K.BROWSER=L([a,l,"major"]),K.CPU=L([d]),K.DEVICE=L([s,c,u,p,f,h,v,g,b]),K.ENGINE=K.OS=L([a,l]),q.exports&&(A=q.exports=K),A.UAParser=K;var H=typeof e!==n&&(e.jQuery||e.Zepto);if(H&&!H.ua){var W=new K;H.ua=W.getResult(),H.ua.get=function(){return W.getUA()},H.ua.set=function(e){W.setUA(e);var t=W.getResult();for(var i in t)H.ua[i]=t[i]}}}("object"==typeof window?window:fe);var he=ve.exports,ge=function(){function e(){this.ua=new ve.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:me(),platform:"Web",os:be(this.ua),deviceModel:ye(this.ua)}},e}(),be=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},ye=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},me=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},we=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),_e=function(){return _e=Object.assign||function(e){for(var t,i=1,n=arguments.length;ie.sessionTimeout&&(t.setSessionId(n),i.session_id=t.getSessionId(),i.time=n),e.lastEventTime=n,[2,i])}))}))}}},bt={page_domain:"".concat(tt," Page Domain"),page_location:"".concat(tt," Page Location"),page_path:"".concat(tt," Page Path"),page_title:"".concat(tt," Page Title"),page_url:"".concat(tt," Page URL")},yt=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.init=function(e,t,n){return void 0===e&&(e=""),K(this._init(i(i({},n),{userId:t,apiKey:e})))},n.prototype._init=function(t){var n,s,u,c,l,d,p,v,h,g,b,m,w,_,I,k,E,S;return r(this,void 0,void 0,(function(){var T,O,x,P,R,N,q,A,D,C,j,L,M,V,z,F,B,$,Q=this;return o(this,(function(K){switch(K.label){case 0:return this.initializing?[2]:(this.initializing=!0,T=t,t.disableCookies?(O="",[3,5]):[3,1]);case 1:return null===(n=t.domain)||void 0===n?[3,2]:(x=n,[3,4]);case 2:return[4,r(void 0,void 0,void 0,(function(){var e,t,i,n,r,s,a;return o(this,(function(o){switch(o.label){case 0:return[4,(new de).isEnabled()];case 1:if(!o.sent()||!W&&"undefined"==typeof location)return[2,""];for(e=null!=W?W:location.hostname,t=e.split("."),i=[],n="AMP_TLDTEST",r=t.length-2;r>=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(Date.now()),z=!0),(F=Te()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new te).promise];case 11:return K.sent(),[4,this.add(new Ue).promise];case 12:return K.sent(),[4,this.add(gt()).promise];case 13:return K.sent(),[4,this.add(new Oe).promise];case 14:return K.sent(),("boolean"==typeof(H=this.config.defaultTracking)?H:null==H?void 0:H.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(ot,((n={})[ut]=o,n[ct]=i.pathname,n[lt]=e.id,n[dt]=e.text,n[pt]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,16];case 15:K.sent(),K.label=16;case 16:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),t.track(rt,((r={})[ft]=e.id,r[vt]=e.name,r[ht]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,18];case 17:K.sent(),K.label=18;case 18:return(null===(_=this.config.attribution)||void 0===_?void 0:_.disabled)?[3,20]:(B=function(){for(var e,t,n=this,s=[],u=0;u ```html diff --git a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js index 6407ee1a7..6907fb8c4 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-MMalHt3JsD6i8mixGQEG238By7xYZ+Wfis8cupNugcLCLPUmgun3P7LiuUQde7ik'; + as.integrity = 'sha384-tUv8KmK/ZnVrRFOEomj4a/y0P/8YBXFXzZLp0z7XtJ4jA21xYr0klVVwzNfbWuCb'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-0.8.2-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.0-min.js.gz'; as.onload = function () { if (!window.amplitudeGTM.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/generated/amplitude-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-snippet.js index ef03328b7..4a646a317 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-45UUPoBhjGCCKb9UnBTn9IDGJAnYh7htP6Wfisz7CZvSa763JxUcV+CW3CTn9k5X'; + as.integrity = 'sha384-tkD0ly9SyHYmmRIB7UNGsvThI7j3D06eYIxxNbb44krcClCTELy2yQgLNxwk58/1'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-0.8.2-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.0-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/package.json b/packages/marketing-analytics-browser/package.json index 7f7a1ca89..e348dad3d 100644 --- a/packages/marketing-analytics-browser/package.json +++ b/packages/marketing-analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/marketing-analytics-browser", - "version": "1.0.0-beta.1", + "version": "1.0.0", "description": "Official Amplitude SDK for Web and Marketing Analytics", "keywords": [ "analytics", @@ -42,12 +42,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.10.6-beta.0", - "@amplitude/analytics-client-common": "^1.0.0-beta.1", - "@amplitude/analytics-core": "^1.0.0-beta.1", - "@amplitude/analytics-types": "^1.0.0-beta.1", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.0-beta.1", - "@amplitude/plugin-web-attribution-browser": "^1.0.0-beta.1", + "@amplitude/analytics-browser": "^1.10.6", + "@amplitude/analytics-client-common": "^1.0.0", + "@amplitude/analytics-core": "^1.0.0", + "@amplitude/analytics-types": "^1.0.0", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.0", + "@amplitude/plugin-web-attribution-browser": "^1.0.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/marketing-analytics-browser/src/version.ts b/packages/marketing-analytics-browser/src/version.ts index 8aa8738be..059488afe 100644 --- a/packages/marketing-analytics-browser/src/version.ts +++ b/packages/marketing-analytics-browser/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.0.0-beta.1'; +export const VERSION = '1.0.0'; diff --git a/packages/plugin-page-view-tracking-browser/CHANGELOG.md b/packages/plugin-page-view-tracking-browser/CHANGELOG.md index 4fbb875ef..2f7ba0442 100644 --- a/packages/plugin-page-view-tracking-browser/CHANGELOG.md +++ b/packages/plugin-page-view-tracking-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.0.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.0-beta.1...@amplitude/plugin-page-view-tracking-browser@1.0.0) (2023-06-06) + +**Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [1.0.0-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@0.8.2...@amplitude/plugin-page-view-tracking-browser@1.0.0-beta.1) (2023-06-06) **Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser diff --git a/packages/plugin-page-view-tracking-browser/package.json b/packages/plugin-page-view-tracking-browser/package.json index e2a56c83a..75dfc80ae 100644 --- a/packages/plugin-page-view-tracking-browser/package.json +++ b/packages/plugin-page-view-tracking-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-page-view-tracking-browser", - "version": "1.0.0-beta.1", + "version": "1.0.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -36,12 +36,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.0-beta.1", - "@amplitude/analytics-types": "^1.0.0-beta.1", + "@amplitude/analytics-client-common": "^1.0.0", + "@amplitude/analytics-types": "^1.0.0", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.10.6-beta.0", + "@amplitude/analytics-browser": "^1.10.6", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", diff --git a/packages/plugin-web-attribution-browser/CHANGELOG.md b/packages/plugin-web-attribution-browser/CHANGELOG.md index 07b239c08..5087729cd 100644 --- a/packages/plugin-web-attribution-browser/CHANGELOG.md +++ b/packages/plugin-web-attribution-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.0.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.0-beta.1...@amplitude/plugin-web-attribution-browser@1.0.0) (2023-06-06) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [1.0.0-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.7.0...@amplitude/plugin-web-attribution-browser@1.0.0-beta.1) (2023-06-06) **Note:** Version bump only for package @amplitude/plugin-web-attribution-browser diff --git a/packages/plugin-web-attribution-browser/package.json b/packages/plugin-web-attribution-browser/package.json index cdf776148..2ac2c998e 100644 --- a/packages/plugin-web-attribution-browser/package.json +++ b/packages/plugin-web-attribution-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-web-attribution-browser", - "version": "1.0.0-beta.1", + "version": "1.0.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -36,12 +36,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.0-beta.1", - "@amplitude/analytics-types": "^1.0.0-beta.1", + "@amplitude/analytics-client-common": "^1.0.0", + "@amplitude/analytics-types": "^1.0.0", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.10.6-beta.0", + "@amplitude/analytics-browser": "^1.10.6", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", From d3091418156cda085f8ad667e8237b904979d059 Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Tue, 6 Jun 2023 00:03:29 -0700 Subject: [PATCH 005/214] build: remove unnecessary beta script guard (#409) --- scripts/version/create-snippet.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/scripts/version/create-snippet.js b/scripts/version/create-snippet.js index 5f5d0e482..f711a520f 100644 --- a/scripts/version/create-snippet.js +++ b/scripts/version/create-snippet.js @@ -4,9 +4,9 @@ const path = require('path'); const { snippet } = require('../templates/browser-snippet.template'); const { getName, getVersion } = require('../utils'); const babel = require('@babel/core'); -const yargs = require('yargs/yargs') -const { hideBin } = require('yargs/helpers') -const argv = yargs(hideBin(process.argv)).argv +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); +const argv = yargs(hideBin(process.argv)).argv; // Setup input const inputDir = 'lib/scripts'; @@ -33,13 +33,11 @@ const encoding = 'base64'; const inputText = fs.readFileSync(inputPath, 'utf-8'); const integrity = algorithm + '-' + crypto.createHash(algorithm).update(inputText).digest(encoding); const version = getVersion() || ''; -const outputText = header + snippet(getName()+nameSuffix, integrity, getVersion(), globalVar); +const outputText = header + snippet(getName() + nameSuffix, integrity, getVersion(), globalVar); const { code: transpiledOutputText } = babel.transformSync(outputText, { presets: ['env'], }); -if (!version.includes('beta')) { - // Write to disk - fs.writeFileSync(outputPath, transpiledOutputText); - console.log(`Generated ${outputFile}`); -} \ No newline at end of file +// Write to disk +fs.writeFileSync(outputPath, transpiledOutputText); +console.log(`Generated ${outputFile}`); From ce17aabe3e0d04386ed201e6b9795ca8d42bd711 Mon Sep 17 00:00:00 2001 From: Justin Fiedler Date: Tue, 6 Jun 2023 11:19:37 -0700 Subject: [PATCH 006/214] fix: don't automatically start new session on setUserId (#400) * fix: allow optionally starting new session on setUserId * fix: update setUserId(id, startNewSession) always start new session if requested * fix: update web-client to allow for `startNewSession` * chore: update session integration tests * chore: test fix * chore: revert interface changes * chore: revert interface changes in tests * chore: update tests for new session change logic * chore: fix set user test --- .../analytics-browser-test/test/index.test.ts | 100 ------------------ .../analytics-browser/src/browser-client.ts | 1 - .../test/browser-client.test.ts | 4 +- 3 files changed, 2 insertions(+), 103 deletions(-) diff --git a/packages/analytics-browser-test/test/index.test.ts b/packages/analytics-browser-test/test/index.test.ts index ac40ad773..9b3072dbd 100644 --- a/packages/analytics-browser-test/test/index.test.ts +++ b/packages/analytics-browser-test/test/index.test.ts @@ -982,16 +982,6 @@ describe('integration', () => { eventType: 'Event in next session', userId: 'user1@amplitude.com', }, - { - eventType: 'session_end', - userId: 'user1@amplitude.com', - }, - ], - [ - { - eventType: 'session_start', - userId: 'user2@amplitude.com', - }, ], ]); expect(payload).toEqual({ @@ -1155,44 +1145,6 @@ describe('integration', () => { user_agent: userAgent, user_id: 'user1@amplitude.com', }, - { - device_id: uuid, - device_manufacturer: undefined, - event_id: 6, - event_type: 'session_end', - insert_id: uuid, - ip: '$remote', - language: 'en-US', - library, - os_name: 'WebKit', - os_version: '537.36', - partner_id: undefined, - plan: undefined, - platform: 'Web', - session_id: number, - time: number, - user_agent: userAgent, - user_id: 'user1@amplitude.com', - }, - { - device_id: uuid, - device_manufacturer: undefined, - event_id: 7, - event_type: 'session_start', - insert_id: uuid, - ip: '$remote', - language: 'en-US', - library, - os_name: 'WebKit', - os_version: '537.36', - partner_id: undefined, - plan: undefined, - platform: 'Web', - session_id: number, - time: number, - user_agent: userAgent, - user_id: 'user2@amplitude.com', - }, ], options: { min_id_length: undefined, @@ -1282,8 +1234,6 @@ describe('integration', () => { expect(deviceIds[2]).toEqual(deviceIds[3]); expect(deviceIds[3]).toEqual(deviceIds[4]); expect(deviceIds[4]).toEqual(deviceIds[5]); - expect(deviceIds[5]).toEqual(deviceIds[6]); - expect(deviceIds[6]).not.toEqual(deviceIds[7]); // The order of events in the payload is sorted by time of track fn invokation // and not consistent with the time property // Session events have overwritten time property @@ -1322,18 +1272,6 @@ describe('integration', () => { userId: 'user1@amplitude.com', deviceId: uuid, }, - { - eventType: 'session_end', - userId: 'user1@amplitude.com', - deviceId: uuid, - }, - ], - [ - { - eventType: 'session_start', - userId: undefined, - deviceId: uuid, - }, ], ]); expect(payload).toEqual({ @@ -1497,44 +1435,6 @@ describe('integration', () => { user_agent: userAgent, user_id: 'user1@amplitude.com', }, - { - device_id: uuid, - device_manufacturer: undefined, - event_id: 6, - event_type: 'session_end', - insert_id: uuid, - ip: '$remote', - language: 'en-US', - library, - os_name: 'WebKit', - os_version: '537.36', - partner_id: undefined, - plan: undefined, - platform: 'Web', - session_id: number, - time: number, - user_agent: userAgent, - user_id: 'user1@amplitude.com', - }, - { - device_id: uuid, - device_manufacturer: undefined, - event_id: 7, - event_type: 'session_start', - insert_id: uuid, - ip: '$remote', - language: 'en-US', - library, - os_name: 'WebKit', - os_version: '537.36', - partner_id: undefined, - plan: undefined, - platform: 'Web', - session_id: number, - time: number, - user_agent: userAgent, - user_id: undefined, - }, ], options: { min_id_length: undefined, diff --git a/packages/analytics-browser/src/browser-client.ts b/packages/analytics-browser/src/browser-client.ts index ab7650bcd..8547a036b 100644 --- a/packages/analytics-browser/src/browser-client.ts +++ b/packages/analytics-browser/src/browser-client.ts @@ -164,7 +164,6 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { } if (userId !== this.config.userId || userId === undefined) { this.config.userId = userId; - this.setSessionId(Date.now()); setConnectorUserId(userId); } } diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index 8c648c8c7..ff77c7879 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -315,7 +315,7 @@ describe('browser-client', () => { expect(client.getUserId()).toBe(USER_ID); }); - test('should set user and session id with start and end session event', async () => { + test('should not send session events on set user', async () => { jest.spyOn(CookieMigration, 'parseLegacyCookies').mockResolvedValueOnce({ optOut: false, sessionId: 1, @@ -347,7 +347,7 @@ describe('browser-client', () => { client.setUserId(undefined); expect(client.getUserId()).toBe(undefined); - expect(track).toHaveBeenCalledTimes(2); + expect(track).toHaveBeenCalledTimes(0); }); test('should defer set user id', () => { From 8f527e4a6437f66640efb95877750dc50ea93f3e Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Tue, 6 Jun 2023 19:04:12 +0000 Subject: [PATCH 007/214] chore(release): publish - @amplitude/analytics-browser-test@1.4.1 - @amplitude/analytics-browser@1.10.7 - @amplitude/marketing-analytics-browser@1.0.1 - @amplitude/plugin-page-view-tracking-browser@1.0.1 - @amplitude/plugin-web-attribution-browser@1.0.1 --- packages/analytics-browser-test/CHANGELOG.md | 13 +++++++++++++ packages/analytics-browser-test/package.json | 4 ++-- packages/analytics-browser/CHANGELOG.md | 13 +++++++++++++ packages/analytics-browser/README.md | 2 +- .../generated/amplitude-snippet.js | 4 ++-- packages/analytics-browser/package.json | 6 +++--- packages/analytics-browser/playground/amplitude.js | 2 +- packages/analytics-browser/src/version.ts | 2 +- packages/marketing-analytics-browser/CHANGELOG.md | 9 +++++++++ packages/marketing-analytics-browser/README.md | 2 +- .../generated/amplitude-gtm-snippet.js | 4 ++-- .../generated/amplitude-snippet.js | 4 ++-- packages/marketing-analytics-browser/package.json | 8 ++++---- packages/marketing-analytics-browser/src/version.ts | 2 +- .../plugin-page-view-tracking-browser/CHANGELOG.md | 9 +++++++++ .../plugin-page-view-tracking-browser/package.json | 4 ++-- .../plugin-web-attribution-browser/CHANGELOG.md | 9 +++++++++ .../plugin-web-attribution-browser/package.json | 4 ++-- 18 files changed, 77 insertions(+), 24 deletions(-) diff --git a/packages/analytics-browser-test/CHANGELOG.md b/packages/analytics-browser-test/CHANGELOG.md index 5a9c21546..0709690b0 100644 --- a/packages/analytics-browser-test/CHANGELOG.md +++ b/packages/analytics-browser-test/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.4.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.0...@amplitude/analytics-browser-test@1.4.1) (2023-06-06) + +### Bug Fixes + +- don't automatically start new session on setUserId + ([#400](https://github.com/amplitude/Amplitude-TypeScript/issues/400)) + ([ce17aab](https://github.com/amplitude/Amplitude-TypeScript/commit/ce17aabe3e0d04386ed201e6b9795ca8d42bd711)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [1.4.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.0-beta.1...@amplitude/analytics-browser-test@1.4.0) (2023-06-06) **Note:** Version bump only for package @amplitude/analytics-browser-test diff --git a/packages/analytics-browser-test/package.json b/packages/analytics-browser-test/package.json index 86af55ecc..7ba79d434 100644 --- a/packages/analytics-browser-test/package.json +++ b/packages/analytics-browser-test/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser-test", - "version": "1.4.0", + "version": "1.4.1", "private": true, "description": "", "author": "Amplitude Inc", @@ -16,7 +16,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.10.6" + "@amplitude/analytics-browser": "^1.10.7" }, "devDependencies": { "nock": "^13.2.4" diff --git a/packages/analytics-browser/CHANGELOG.md b/packages/analytics-browser/CHANGELOG.md index 9af42e906..774ddc1a2 100644 --- a/packages/analytics-browser/CHANGELOG.md +++ b/packages/analytics-browser/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.10.7](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.10.6...@amplitude/analytics-browser@1.10.7) (2023-06-06) + +### Bug Fixes + +- don't automatically start new session on setUserId + ([#400](https://github.com/amplitude/Amplitude-TypeScript/issues/400)) + ([ce17aab](https://github.com/amplitude/Amplitude-TypeScript/commit/ce17aabe3e0d04386ed201e6b9795ca8d42bd711)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.10.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.10.6-beta.0...@amplitude/analytics-browser@1.10.6) (2023-06-06) **Note:** Version bump only for package @amplitude/analytics-browser diff --git a/packages/analytics-browser/README.md b/packages/analytics-browser/README.md index ca05dd049..41237ac9a 100644 --- a/packages/analytics-browser/README.md +++ b/packages/analytics-browser/README.md @@ -40,7 +40,7 @@ Alternatively, the package is also distributed through a CDN. Copy and paste the ```html diff --git a/packages/analytics-browser/generated/amplitude-snippet.js b/packages/analytics-browser/generated/amplitude-snippet.js index 18820c265..ea820219a 100644 --- a/packages/analytics-browser/generated/amplitude-snippet.js +++ b/packages/analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-/pEyHhmhzhlIzPsIq1ptBvg2RLgkHo5wAhA7DEi6bJ+jNOQ7Us/bz8mfKQ8hGyZX'; + as.integrity = 'sha384-1RCGOEWBysSvMhkUFt9WiJ1yWQmI9CK/Epcmvz6EIhnaXEelKj6DQyduxW13vrki'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.10.6-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.10.7-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/analytics-browser/package.json b/packages/analytics-browser/package.json index 6d3345b8e..015831d5b 100644 --- a/packages/analytics-browser/package.json +++ b/packages/analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser", - "version": "1.10.6", + "version": "1.10.7", "description": "Official Amplitude SDK for Web", "keywords": [ "analytics", @@ -46,8 +46,8 @@ "@amplitude/analytics-client-common": "^1.0.0", "@amplitude/analytics-core": "^1.0.0", "@amplitude/analytics-types": "^1.0.0", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.0", - "@amplitude/plugin-web-attribution-browser": "^1.0.0", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.1", + "@amplitude/plugin-web-attribution-browser": "^1.0.1", "@amplitude/ua-parser-js": "^0.7.31", "tslib": "^2.4.1" }, diff --git a/packages/analytics-browser/playground/amplitude.js b/packages/analytics-browser/playground/amplitude.js index e0aa00876..1d15830bb 100644 --- a/packages/analytics-browser/playground/amplitude.js +++ b/packages/analytics-browser/playground/amplitude.js @@ -1 +1 @@ -!function(){"use strict";var e=function(t,i){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])},e(t,i)};function t(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=t}e(t,i),t.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var i=function(){return i=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!V(t,i))return!1}return!0},V=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&M(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return M(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},z=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||V(t,i)))},e}(),F=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},B=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},$=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return B(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),Q="Event rejected due to exceeded retry count",K=function(e){return{promise:e||Promise.resolve()}},H=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new $(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return K(this.dispatch(r))},e.prototype.identify=function(e,t){var i=F(e,t);return K(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return K(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new z;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return K(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return K(this.dispatch(n))},e.prototype.add=function(e){return this.config?K(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),K())},e.prototype.remove=function(e){return this.config?K(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),K())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(B(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,B(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=B(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return K(this.timeline.flush())},e}(),W=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return M(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),Z="Amplitude Logger ",G=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=ee(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:if(null===(s=o.sent()))return this.fulfillRequest(e,0,"Unexpected error occurred"),[2];if(!t){if("body"in s){a="";try{a=JSON.stringify(s.body,null,2)}catch(e){}this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(a))}else this.fulfillRequest(e,s.statusCode,s.status);return[2]}return this.handleReponse(s,e),[3,4];case 3:return u=o.sent(),this.fulfillRequest(e,0,String(u)),[3,4];case 4:return[2]}}))}))},e.prototype.handleReponse=function(e,t){switch(e.status){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.handleOtherReponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherReponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(B(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ie=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},ne=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},re=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},oe=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=re(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},se=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ae)},ue=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),ce=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),le=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[C,t,e.substring(0,i)].filter(Boolean).join("_")},de=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),pe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(ce),fe="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},ve={exports:{}};q=ve,A=ve.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",D="Xiaomi",C="Zebra",j="Facebook",L=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},B=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?z(e,275):e,this},this.setUA(f),this};K.VERSION="0.7.31",K.BROWSER=L([a,l,"major"]),K.CPU=L([d]),K.DEVICE=L([s,c,u,p,f,h,v,g,b]),K.ENGINE=K.OS=L([a,l]),q.exports&&(A=q.exports=K),A.UAParser=K;var H=typeof e!==n&&(e.jQuery||e.Zepto);if(H&&!H.ua){var W=new K;H.ua=W.getResult(),H.ua.get=function(){return W.getUA()},H.ua.set=function(e){W.setUA(e);var t=W.getResult();for(var i in t)H.ua[i]=t[i]}}}("object"==typeof window?window:fe);var he=ve.exports,ge=function(){function e(){this.ua=new ve.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:me(),platform:"Web",os:be(this.ua),deviceModel:ye(this.ua)}},e}(),be=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},ye=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},me=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},we=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),_e=function(){return _e=Object.assign||function(e){for(var t,i=1,n=arguments.length;ie.sessionTimeout&&(t.setSessionId(n),i.session_id=t.getSessionId(),i.time=n),e.lastEventTime=n,[2,i])}))}))}}},bt={page_domain:"".concat(tt," Page Domain"),page_location:"".concat(tt," Page Location"),page_path:"".concat(tt," Page Path"),page_title:"".concat(tt," Page Title"),page_url:"".concat(tt," Page URL")},yt=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.init=function(e,t,n){return void 0===e&&(e=""),K(this._init(i(i({},n),{userId:t,apiKey:e})))},n.prototype._init=function(t){var n,s,u,c,l,d,p,v,h,g,b,m,w,_,I,k,E,S;return r(this,void 0,void 0,(function(){var T,O,x,P,R,N,q,A,D,C,j,L,M,V,z,F,B,$,Q=this;return o(this,(function(K){switch(K.label){case 0:return this.initializing?[2]:(this.initializing=!0,T=t,t.disableCookies?(O="",[3,5]):[3,1]);case 1:return null===(n=t.domain)||void 0===n?[3,2]:(x=n,[3,4]);case 2:return[4,r(void 0,void 0,void 0,(function(){var e,t,i,n,r,s,a;return o(this,(function(o){switch(o.label){case 0:return[4,(new de).isEnabled()];case 1:if(!o.sent()||!W&&"undefined"==typeof location)return[2,""];for(e=null!=W?W:location.hostname,t=e.split("."),i=[],n="AMP_TLDTEST",r=t.length-2;r>=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(Date.now()),z=!0),(F=Te()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new te).promise];case 11:return K.sent(),[4,this.add(new Ue).promise];case 12:return K.sent(),[4,this.add(gt()).promise];case 13:return K.sent(),[4,this.add(new Oe).promise];case 14:return K.sent(),("boolean"==typeof(H=this.config.defaultTracking)?H:null==H?void 0:H.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(ot,((n={})[ut]=o,n[ct]=i.pathname,n[lt]=e.id,n[dt]=e.text,n[pt]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,16];case 15:K.sent(),K.label=16;case 16:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),t.track(rt,((r={})[ft]=e.id,r[vt]=e.name,r[ht]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,18];case 17:K.sent(),K.label=18;case 18:return(null===(_=this.config.attribution)||void 0===_?void 0:_.disabled)?[3,20]:(B=function(){for(var e,t,n=this,s=[],u=0;u0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!V(t,i))return!1}return!0},V=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&M(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return M(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},z=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||V(t,i)))},e}(),F=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},B=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},$=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return B(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),Q="Event rejected due to exceeded retry count",K=function(e){return{promise:e||Promise.resolve()}},H=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new $(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return K(this.dispatch(r))},e.prototype.identify=function(e,t){var i=F(e,t);return K(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return K(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new z;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return K(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return K(this.dispatch(n))},e.prototype.add=function(e){return this.config?K(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),K())},e.prototype.remove=function(e){return this.config?K(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),K())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(B(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,B(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=B(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return K(this.timeline.flush())},e}(),W=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return M(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),Z="Amplitude Logger ",G=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=ee(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:if(null===(s=o.sent()))return this.fulfillRequest(e,0,"Unexpected error occurred"),[2];if(!t){if("body"in s){a="";try{a=JSON.stringify(s.body,null,2)}catch(e){}this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(a))}else this.fulfillRequest(e,s.statusCode,s.status);return[2]}return this.handleReponse(s,e),[3,4];case 3:return u=o.sent(),this.fulfillRequest(e,0,String(u)),[3,4];case 4:return[2]}}))}))},e.prototype.handleReponse=function(e,t){switch(e.status){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.handleOtherReponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherReponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(B(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ie=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},ne=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},re=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},oe=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=re(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},se=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ae)},ue=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),ce=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),le=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[C,t,e.substring(0,i)].filter(Boolean).join("_")},de=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),pe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(ce),fe="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},ve={exports:{}};q=ve,A=ve.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",D="Xiaomi",C="Zebra",j="Facebook",L=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},B=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?z(e,275):e,this},this.setUA(f),this};K.VERSION="0.7.31",K.BROWSER=L([a,l,"major"]),K.CPU=L([d]),K.DEVICE=L([s,c,u,p,f,h,v,g,b]),K.ENGINE=K.OS=L([a,l]),q.exports&&(A=q.exports=K),A.UAParser=K;var H=typeof e!==n&&(e.jQuery||e.Zepto);if(H&&!H.ua){var W=new K;H.ua=W.getResult(),H.ua.get=function(){return W.getUA()},H.ua.set=function(e){W.setUA(e);var t=W.getResult();for(var i in t)H.ua[i]=t[i]}}}("object"==typeof window?window:fe);var he=ve.exports,ge=function(){function e(){this.ua=new ve.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:me(),platform:"Web",os:be(this.ua),deviceModel:ye(this.ua)}},e}(),be=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},ye=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},me=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},we=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),_e=function(){return _e=Object.assign||function(e){for(var t,i=1,n=arguments.length;ie.sessionTimeout&&(t.setSessionId(n),i.session_id=t.getSessionId(),i.time=n),e.lastEventTime=n,[2,i])}))}))}}},bt={page_domain:"".concat(tt," Page Domain"),page_location:"".concat(tt," Page Location"),page_path:"".concat(tt," Page Path"),page_title:"".concat(tt," Page Title"),page_url:"".concat(tt," Page URL")},yt=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.init=function(e,t,n){return void 0===e&&(e=""),K(this._init(i(i({},n),{userId:t,apiKey:e})))},n.prototype._init=function(t){var n,s,u,c,l,d,p,v,h,g,b,m,w,_,I,k,E,S;return r(this,void 0,void 0,(function(){var T,O,x,P,R,N,q,A,D,C,j,L,M,V,z,F,B,$,Q=this;return o(this,(function(K){switch(K.label){case 0:return this.initializing?[2]:(this.initializing=!0,T=t,t.disableCookies?(O="",[3,5]):[3,1]);case 1:return null===(n=t.domain)||void 0===n?[3,2]:(x=n,[3,4]);case 2:return[4,r(void 0,void 0,void 0,(function(){var e,t,i,n,r,s,a;return o(this,(function(o){switch(o.label){case 0:return[4,(new de).isEnabled()];case 1:if(!o.sent()||!W&&"undefined"==typeof location)return[2,""];for(e=null!=W?W:location.hostname,t=e.split("."),i=[],n="AMP_TLDTEST",r=t.length-2;r>=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(Date.now()),z=!0),(F=Te()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new te).promise];case 11:return K.sent(),[4,this.add(new Ue).promise];case 12:return K.sent(),[4,this.add(gt()).promise];case 13:return K.sent(),[4,this.add(new Oe).promise];case 14:return K.sent(),("boolean"==typeof(H=this.config.defaultTracking)?H:null==H?void 0:H.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(ot,((n={})[ut]=o,n[ct]=i.pathname,n[lt]=e.id,n[dt]=e.text,n[pt]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,16];case 15:K.sent(),K.label=16;case 16:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),t.track(rt,((r={})[ft]=e.id,r[vt]=e.name,r[ht]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,18];case 17:K.sent(),K.label=18;case 18:return(null===(_=this.config.attribution)||void 0===_?void 0:_.disabled)?[3,20]:(B=function(){for(var e,t,n=this,s=[],u=0;u ```html diff --git a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js index 6907fb8c4..513249106 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-tUv8KmK/ZnVrRFOEomj4a/y0P/8YBXFXzZLp0z7XtJ4jA21xYr0klVVwzNfbWuCb'; + as.integrity = 'sha384-mE2nuVUBrzajX9YbPZ8uOzONHjss0X7Z2RNoREFg60vDNS3SUPtSxZclXOIXu34H'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.0-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.1-min.js.gz'; as.onload = function () { if (!window.amplitudeGTM.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/generated/amplitude-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-snippet.js index 4a646a317..f8911227d 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-tkD0ly9SyHYmmRIB7UNGsvThI7j3D06eYIxxNbb44krcClCTELy2yQgLNxwk58/1'; + as.integrity = 'sha384-U6E9CMsOG+jKnR35qw+9MKntJySFcpJWIl3ultGC94Bq51pzfLpEavF79VTqG5iU'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.0-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.1-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/package.json b/packages/marketing-analytics-browser/package.json index e348dad3d..bcec4c930 100644 --- a/packages/marketing-analytics-browser/package.json +++ b/packages/marketing-analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/marketing-analytics-browser", - "version": "1.0.0", + "version": "1.0.1", "description": "Official Amplitude SDK for Web and Marketing Analytics", "keywords": [ "analytics", @@ -42,12 +42,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.10.6", + "@amplitude/analytics-browser": "^1.10.7", "@amplitude/analytics-client-common": "^1.0.0", "@amplitude/analytics-core": "^1.0.0", "@amplitude/analytics-types": "^1.0.0", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.0", - "@amplitude/plugin-web-attribution-browser": "^1.0.0", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.1", + "@amplitude/plugin-web-attribution-browser": "^1.0.1", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/marketing-analytics-browser/src/version.ts b/packages/marketing-analytics-browser/src/version.ts index 059488afe..e96d2876d 100644 --- a/packages/marketing-analytics-browser/src/version.ts +++ b/packages/marketing-analytics-browser/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.0.0'; +export const VERSION = '1.0.1'; diff --git a/packages/plugin-page-view-tracking-browser/CHANGELOG.md b/packages/plugin-page-view-tracking-browser/CHANGELOG.md index 2f7ba0442..7f184b2bd 100644 --- a/packages/plugin-page-view-tracking-browser/CHANGELOG.md +++ b/packages/plugin-page-view-tracking-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.0...@amplitude/plugin-page-view-tracking-browser@1.0.1) (2023-06-06) + +**Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [1.0.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.0-beta.1...@amplitude/plugin-page-view-tracking-browser@1.0.0) (2023-06-06) **Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser diff --git a/packages/plugin-page-view-tracking-browser/package.json b/packages/plugin-page-view-tracking-browser/package.json index 75dfc80ae..9742188db 100644 --- a/packages/plugin-page-view-tracking-browser/package.json +++ b/packages/plugin-page-view-tracking-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-page-view-tracking-browser", - "version": "1.0.0", + "version": "1.0.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -41,7 +41,7 @@ "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.10.6", + "@amplitude/analytics-browser": "^1.10.7", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", diff --git a/packages/plugin-web-attribution-browser/CHANGELOG.md b/packages/plugin-web-attribution-browser/CHANGELOG.md index 5087729cd..556c52fbf 100644 --- a/packages/plugin-web-attribution-browser/CHANGELOG.md +++ b/packages/plugin-web-attribution-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.0...@amplitude/plugin-web-attribution-browser@1.0.1) (2023-06-06) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [1.0.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.0-beta.1...@amplitude/plugin-web-attribution-browser@1.0.0) (2023-06-06) **Note:** Version bump only for package @amplitude/plugin-web-attribution-browser diff --git a/packages/plugin-web-attribution-browser/package.json b/packages/plugin-web-attribution-browser/package.json index 2ac2c998e..d00fb2b68 100644 --- a/packages/plugin-web-attribution-browser/package.json +++ b/packages/plugin-web-attribution-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-web-attribution-browser", - "version": "1.0.0", + "version": "1.0.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -41,7 +41,7 @@ "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.10.6", + "@amplitude/analytics-browser": "^1.10.7", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", From a48ddc55cf578727256108e5983172942d419c0e Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Thu, 8 Jun 2023 15:28:43 -0700 Subject: [PATCH 008/214] docs: update instructions and docs to reference 1.0 (#419) --- packages/analytics-browser/README.md | 4 ++-- packages/analytics-node/README.md | 4 ++-- packages/analytics-react-native/README.md | 4 ++-- packages/marketing-analytics-browser/README.md | 4 ++-- packages/plugin-page-view-tracking-browser/README.md | 6 +++--- packages/plugin-web-attribution-browser/README.md | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/analytics-browser/README.md b/packages/analytics-browser/README.md index 41237ac9a..ce7fef47c 100644 --- a/packages/analytics-browser/README.md +++ b/packages/analytics-browser/README.md @@ -27,10 +27,10 @@ This package is published on NPM registry and is available to be installed using ```sh # npm -npm install @amplitude/analytics-browser +npm install @amplitude/analytics-browser@^1.0.0 # yarn -yarn add @amplitude/analytics-browser +yarn add @amplitude/analytics-browser@^1.0.0 ``` ### Using script loader diff --git a/packages/analytics-node/README.md b/packages/analytics-node/README.md index 5a743cc43..86e00d75f 100644 --- a/packages/analytics-node/README.md +++ b/packages/analytics-node/README.md @@ -27,10 +27,10 @@ This package is published on NPM registry and is available to be installed using ```sh # npm -npm install @amplitude/analytics-node +npm install @amplitude/analytics-node@^1.0.0 # yarn -yarn add @amplitude/analytics-node +yarn add @amplitude/analytics-node@^1.0.0 ``` ## Usage diff --git a/packages/analytics-react-native/README.md b/packages/analytics-react-native/README.md index 8b3c66b3b..a021e6e5e 100644 --- a/packages/analytics-react-native/README.md +++ b/packages/analytics-react-native/README.md @@ -19,11 +19,11 @@ To get started with using Amplitude React Native SDK, install the package to you ```sh # npm -npm install @amplitude/analytics-react-native +npm install @amplitude/analytics-react-native@^1.0.0 npm install @react-native-async-storage/async-storage # yarn -yarn add @amplitude/analytics-react-native +yarn add @amplitude/analytics-react-native@^1.0.0 yarn add @react-native-async-storage/async-storage ``` diff --git a/packages/marketing-analytics-browser/README.md b/packages/marketing-analytics-browser/README.md index 089dbb291..f3d9bc589 100644 --- a/packages/marketing-analytics-browser/README.md +++ b/packages/marketing-analytics-browser/README.md @@ -27,10 +27,10 @@ This package is published on NPM registry and is available to be installed using ```sh # npm -npm install @amplitude/marketing-analytics-browser +npm install @amplitude/marketing-analytics-browser@^1.0.0 # yarn -yarn add @amplitude/marketing-analytics-browser +yarn add @amplitude/marketing-analytics-browser@^1.0.0 ``` ### Using script loader diff --git a/packages/plugin-page-view-tracking-browser/README.md b/packages/plugin-page-view-tracking-browser/README.md index 601033034..7bf59d1ca 100644 --- a/packages/plugin-page-view-tracking-browser/README.md +++ b/packages/plugin-page-view-tracking-browser/README.md @@ -15,10 +15,10 @@ This package is published on NPM registry and is available to be installed using ```sh # npm -npm install @amplitude/plugin-page-view-tracking-browser +npm install @amplitude/plugin-page-view-tracking-browser@^1.0.0 # yarn -yarn add @amplitude/plugin-page-view-tracking-browser +yarn add @amplitude/plugin-page-view-tracking-browser@^1.0.0 ``` ## Usage @@ -50,7 +50,7 @@ const pageViewTracking = pageViewTrackingPlugin(amplitude, { |Name|Type|Default|Description| |-|-|-|-| -|`trackOn`|`"attribution"` or `() => boolean`|`undefined`|Use this option to control when to track a page view event. By default, a page view event is sent on each SDK initialization.

Use `() => boolean` to control sending page view events using custom conditional logic.

Use `"attribution"` to send page view events with attribution events. This option requires using [@amplitude/plugin-web-attribution-browser](https://github.com/amplitude/Amplitude-TypeScript/tree/main/packages/plugin-web-attribution-browser).| +|`trackOn`|`"attribution"` or `() => boolean`|`undefined`|Use this option to control when to track a page view event. By default, a page view event is sent on each SDK initialization.

Use `() => boolean` to control sending page view events using custom conditional logic.

Use `"attribution"` to send page view events with attribution events. This option requires using [@amplitude/plugin-web-attribution-browser](https://github.com/amplitude/Amplitude-TypeScript/tree/v1.x/packages/plugin-web-attribution-browser).| |`trackHistoryChanges`|`"all"` or `"pathOnly"`|`undefined`|Use this option to subscribe to page view changes in a single page application like React.js. By default, page view changes in single page applications does not trigger a page view event.

Use `"all"` to compare the full url changes.

Use `"pathOnly"` to compare only url path changes.| ### 3. Install plugin to Amplitude SDK diff --git a/packages/plugin-web-attribution-browser/README.md b/packages/plugin-web-attribution-browser/README.md index 2cffd9ced..e95274e74 100644 --- a/packages/plugin-web-attribution-browser/README.md +++ b/packages/plugin-web-attribution-browser/README.md @@ -15,10 +15,10 @@ This package is published on NPM registry and is available to be installed using ```sh # npm -npm install @amplitude/plugin-web-attribution-browser +npm install @amplitude/plugin-web-attribution-browser@^1.0.0 # yarn -yarn add @amplitude/plugin-web-attribution-browser +yarn add @amplitude/plugin-web-attribution-browser@^1.0.0 ``` ## Usage From 86de7bd75c564601d980c029e548fe0303ba43f2 Mon Sep 17 00:00:00 2001 From: Justin Fiedler Date: Thu, 8 Jun 2023 16:19:28 -0700 Subject: [PATCH 009/214] feat: log response body from API to logger (#415) * feat: log response body from API to logger * fix: fix test for 429 status, extend 'log unexpected error' test to increase coverage * fix: add status code to response logging * chore: typo * fix: reject send errors * chore: resolve promise for req.on('error'), remove throw error from SDK * chore: remove superfluous logging on success * chore: only log intermediate responses prior to retry * chore: remove duplicate logging on "other": response * chore: clean up test logic --------- Co-authored-by: Andrey Sokolov --- .../analytics-core/src/plugins/destination.ts | 72 ++++-- .../test/plugins/destination.test.ts | 208 +++++++++++++++++- .../analytics-node/src/transports/http.ts | 2 +- packages/analytics-types/src/response.ts | 4 +- 4 files changed, 260 insertions(+), 26 deletions(-) diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index 3781fdb45..602bf1c35 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -24,6 +24,23 @@ import { chunk } from '../utils/chunk'; import { buildResult } from '../utils/result-builder'; import { createServerConfig } from '../config'; +function getErrorMessage(error: unknown) { + if (error instanceof Error) return error.message; + return String(error); +} + +export function getResponseBodyString(res: Response) { + let responseBodyString = ''; + try { + if ('body' in res) { + responseBodyString = JSON.stringify(res.body, null, 2); + } + } catch { + // to avoid crash, but don't care about the error, add comment to avoid empty block lint error + } + return responseBodyString; +} + export class Destination implements DestinationPlugin { name = 'amplitude'; type = PluginType.DESTINATION as const; @@ -141,45 +158,47 @@ export class Destination implements DestinationPlugin { } if (!useRetry) { if ('body' in res) { - let responseBody = ''; - try { - responseBody = JSON.stringify(res.body, null, 2); - } catch { - // to avoid crash, but don't care about the error, add comment to avoid empty block lint error - } - this.fulfillRequest(list, res.statusCode, `${res.status}: ${responseBody}`); + this.fulfillRequest(list, res.statusCode, `${res.status}: ${getResponseBodyString(res)}`); } else { this.fulfillRequest(list, res.statusCode, res.status); } return; } - this.handleReponse(res, list); + this.handleResponse(res, list); } catch (e) { - this.fulfillRequest(list, 0, String(e)); + const errorMessage = getErrorMessage(e); + this.config.loggerProvider.error(errorMessage); + this.fulfillRequest(list, 0, errorMessage); } } - handleReponse(res: Response, list: Context[]) { + handleResponse(res: Response, list: Context[]) { const { status } = res; + switch (status) { - case Status.Success: + case Status.Success: { this.handleSuccessResponse(res, list); break; - - case Status.Invalid: + } + case Status.Invalid: { this.handleInvalidResponse(res, list); break; - - case Status.PayloadTooLarge: + } + case Status.PayloadTooLarge: { this.handlePayloadTooLargeResponse(res, list); break; - - case Status.RateLimit: + } + case Status.RateLimit: { this.handleRateLimitResponse(res, list); break; + } + default: { + // log intermediate event status before retry + this.config.loggerProvider.warn(`{code: 0, error: "Status '${status}' provided for ${list.length} events"}`); - default: - this.handleOtherReponse(list); + this.handleOtherResponse(list); + break; + } } } @@ -209,6 +228,10 @@ export class Destination implements DestinationPlugin { return true; }); + if (retry.length > 0) { + // log intermediate event status before retry + this.config.loggerProvider.warn(getResponseBodyString(res)); + } this.addToQueue(...retry); } @@ -217,6 +240,10 @@ export class Destination implements DestinationPlugin { this.fulfillRequest(list, res.statusCode, res.body.error); return; } + + // log intermediate event status before retry + this.config.loggerProvider.warn(getResponseBodyString(res)); + this.config.flushQueueSize /= 2; this.addToQueue(...list); } @@ -243,10 +270,15 @@ export class Destination implements DestinationPlugin { return true; }); + if (retry.length > 0) { + // log intermediate event status before retry + this.config.loggerProvider.warn(getResponseBodyString(res)); + } + this.addToQueue(...retry); } - handleOtherReponse(list: Context[]) { + handleOtherResponse(list: Context[]) { this.addToQueue( ...list.map((context) => { context.timeout = context.attempts * this.retryTimeout; diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index c3635d33b..41345a513 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -1,5 +1,5 @@ -import { Destination } from '../../src/plugins/destination'; -import { DestinationContext, Payload, Status } from '@amplitude/analytics-types'; +import { Destination, getResponseBodyString } from '../../src/plugins/destination'; +import { Config, DestinationContext, Logger, Payload, Result, Status } from '@amplitude/analytics-types'; import { API_KEY, useDefaultConfig } from '../helpers/default'; import { INVALID_API_KEY, @@ -8,6 +8,8 @@ import { UNEXPECTED_ERROR_MESSAGE, } from '../../src/messages'; +const jsons = (obj: any) => JSON.stringify(obj, null, 2); + describe('destination', () => { describe('setup', () => { test('should setup plugin', async () => { @@ -373,7 +375,7 @@ describe('destination', () => { expect(callback).toHaveBeenCalledWith({ event, code: 400, - message: `${Status.Invalid}: ${JSON.stringify(body, null, 2)}`, + message: `${Status.Invalid}: ${jsons(body)}`, }); }); @@ -833,4 +835,204 @@ describe('destination', () => { expect(transportProvider.send).toHaveBeenCalledTimes(1); }); }); + + describe('logging', () => { + const getMockLogger = (): Logger => ({ + log: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + enable: jest.fn(), + disable: jest.fn(), + }); + + test('should handle null loggerProvider', async () => { + class Http { + send = jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ + status: Status.Success, + statusCode: 200, + body: { + message: 'success', + }, + }); + }); + } + + const transportProvider = new Http(); + const destination = new Destination(); + + const config: Config = { + ...useDefaultConfig(), + flushQueueSize: 1, + flushIntervalMillis: 1, + transportProvider, + }; + await destination.setup(config); + const event = { + event_type: 'event_type', + }; + const result = await destination.execute(event); + expect(result).toEqual({ + event, + message: SUCCESS_MESSAGE, + code: 200, + }); + expect(transportProvider.send).toHaveBeenCalledTimes(1); + }); + + test.each([ + { + statusCode: 400, + status: Status.Invalid, + body: { + code: 400, + error: 'error', + missingField: undefined, + eventsWithInvalidFields: {}, + eventsWithMissingFields: {}, + eventsWithInvalidIdLengths: {}, + silencedEvents: [], + }, + }, + { + statusCode: 413, + status: Status.PayloadTooLarge, + body: { + code: 413, + error: 'error', + }, + }, + { + statusCode: 429, + status: Status.RateLimit, + body: { + code: 429, + error: 'error', + epsThreshold: 1, + throttledDevices: {}, + throttledUsers: {}, + exceededDailyQuotaDevices: { + '1': 1, + }, + exceededDailyQuotaUsers: { + '2': 1, + }, + throttledEvents: [0], + }, + }, + ])( + 'should log intermediate response body for retries of $statusCode $status', + async ({ statusCode, status, body }) => { + const response = { + status, + statusCode, + body, + }; + + class Http { + send = jest + .fn() + .mockImplementationOnce(() => { + return Promise.resolve(response); + }) + .mockImplementationOnce(() => { + return Promise.resolve({ + status: Status.Success, + statusCode: 200, + body: { + message: SUCCESS_MESSAGE, + }, + }); + }); + } + const transportProvider = new Http(); + const destination = new Destination(); + const loggerProvider = getMockLogger(); + const eventCount = status === Status.PayloadTooLarge ? 2 : 1; + const config = { + ...useDefaultConfig(), + flushQueueSize: eventCount, + flushIntervalMillis: 1, + transportProvider, + loggerProvider, + }; + await destination.setup(config); + destination.retryTimeout = 10; + destination.throttleTimeout = 1; + const event = { + event_type: 'event_type', + }; + let result; + if (eventCount > 1) { + // Need 2 events for 413 to retry, send them both at the same time + result = await new Promise((resolve) => { + const context: DestinationContext = { + event, + attempts: 0, + callback: (result: Result) => resolve(result), + timeout: 0, + }; + void destination.addToQueue(context, context); + }); + } else { + result = await destination.execute(event); + } + + expect(result).toEqual({ + event, + message: SUCCESS_MESSAGE, + code: 200, + }); + + expect(transportProvider.send).toHaveBeenCalledTimes(eventCount + 1); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(loggerProvider.warn).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method,@typescript-eslint/restrict-template-expressions + expect(loggerProvider.warn).toHaveBeenCalledWith(jsons(response.body)); + }, + ); + + test.each([ + { err: new Error('Error'), message: 'Error' }, + { err: 'string error', message: 'string error' }, + ])('should log unexpected error "$message"', async ({ err, message }) => { + const destination = new Destination(); + const callback = jest.fn(); + const context = { + attempts: 0, + callback, + event: { + event_type: 'event_type', + }, + timeout: 0, + }; + const transportProvider = { + send: jest.fn().mockImplementationOnce(() => { + throw err; + }), + }; + const loggerProvider = getMockLogger(); + + await destination.setup({ + ...useDefaultConfig(), + transportProvider, + loggerProvider, + }); + await destination.send([context]); + expect(callback).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(loggerProvider.error).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(loggerProvider.error).toHaveBeenCalledWith(message); + }); + + test('should parse response without body', async () => { + const result = getResponseBodyString({ + status: Status.Unknown, + statusCode: 700, + }); + expect(result).toBe(''); + }); + }); }); diff --git a/packages/analytics-node/src/transports/http.ts b/packages/analytics-node/src/transports/http.ts index 1c44795f2..0c0c985b0 100644 --- a/packages/analytics-node/src/transports/http.ts +++ b/packages/analytics-node/src/transports/http.ts @@ -49,7 +49,7 @@ export class Http extends BaseTransport implements Transport { } }); }); - req.on('error', this.buildResponse.bind(this)); + req.on('error', () => resolve(null)); req.end(requestPayload); }); } diff --git a/packages/analytics-types/src/response.ts b/packages/analytics-types/src/response.ts index f2e8f4554..04e6108e5 100644 --- a/packages/analytics-types/src/response.ts +++ b/packages/analytics-types/src/response.ts @@ -65,7 +65,7 @@ export interface TimeoutResponse { statusCode: number; } -export interface OtherReponse { +export interface OtherResponse { status: Exclude; statusCode: number; } @@ -76,4 +76,4 @@ export type Response = | PayloadTooLargeResponse | RateLimitResponse | TimeoutResponse - | OtherReponse; + | OtherResponse; From 0c03a57239e1048b1730fa58464b125ba9b8b6ba Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 8 Jun 2023 23:33:23 +0000 Subject: [PATCH 010/214] chore(release): publish - @amplitude/analytics-browser-test@1.4.2 - @amplitude/analytics-browser@1.10.8 - @amplitude/analytics-client-common@1.0.1 - @amplitude/analytics-core@1.1.0 - @amplitude/analytics-node-test@1.0.1 - @amplitude/analytics-node@1.2.0 - @amplitude/analytics-react-native@1.1.9 - @amplitude/analytics-types@1.1.0 - @amplitude/marketing-analytics-browser@1.0.2 - @amplitude/plugin-page-view-tracking-browser@1.0.2 - @amplitude/plugin-web-attribution-browser@1.0.2 --- packages/analytics-browser-test/CHANGELOG.md | 9 +++++++++ packages/analytics-browser-test/package.json | 4 ++-- packages/analytics-browser/CHANGELOG.md | 9 +++++++++ packages/analytics-browser/README.md | 2 +- .../generated/amplitude-snippet.js | 4 ++-- packages/analytics-browser/package.json | 12 ++++++------ packages/analytics-browser/playground/amplitude.js | 2 +- packages/analytics-browser/src/version.ts | 2 +- packages/analytics-client-common/CHANGELOG.md | 9 +++++++++ packages/analytics-client-common/package.json | 6 +++--- packages/analytics-core/CHANGELOG.md | 12 ++++++++++++ packages/analytics-core/package.json | 4 ++-- packages/analytics-node-test/CHANGELOG.md | 9 +++++++++ packages/analytics-node-test/package.json | 4 ++-- packages/analytics-node/CHANGELOG.md | 12 ++++++++++++ packages/analytics-node/package.json | 6 +++--- packages/analytics-node/src/version.ts | 2 +- packages/analytics-react-native/CHANGELOG.md | 9 +++++++++ packages/analytics-react-native/package.json | 8 ++++---- packages/analytics-react-native/src/version.ts | 2 +- packages/analytics-types/CHANGELOG.md | 12 ++++++++++++ packages/analytics-types/package.json | 2 +- packages/marketing-analytics-browser/CHANGELOG.md | 9 +++++++++ packages/marketing-analytics-browser/README.md | 2 +- .../generated/amplitude-gtm-snippet.js | 4 ++-- .../generated/amplitude-snippet.js | 4 ++-- packages/marketing-analytics-browser/package.json | 14 +++++++------- .../marketing-analytics-browser/src/version.ts | 2 +- .../plugin-page-view-tracking-browser/CHANGELOG.md | 9 +++++++++ .../plugin-page-view-tracking-browser/package.json | 8 ++++---- .../plugin-web-attribution-browser/CHANGELOG.md | 9 +++++++++ .../plugin-web-attribution-browser/package.json | 8 ++++---- 32 files changed, 159 insertions(+), 51 deletions(-) diff --git a/packages/analytics-browser-test/CHANGELOG.md b/packages/analytics-browser-test/CHANGELOG.md index 0709690b0..ebf9e1d7c 100644 --- a/packages/analytics-browser-test/CHANGELOG.md +++ b/packages/analytics-browser-test/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.4.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.1...@amplitude/analytics-browser-test@1.4.2) (2023-06-08) + +**Note:** Version bump only for package @amplitude/analytics-browser-test + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.4.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.0...@amplitude/analytics-browser-test@1.4.1) (2023-06-06) ### Bug Fixes diff --git a/packages/analytics-browser-test/package.json b/packages/analytics-browser-test/package.json index 7ba79d434..c20f27036 100644 --- a/packages/analytics-browser-test/package.json +++ b/packages/analytics-browser-test/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser-test", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "", "author": "Amplitude Inc", @@ -16,7 +16,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.10.7" + "@amplitude/analytics-browser": "^1.10.8" }, "devDependencies": { "nock": "^13.2.4" diff --git a/packages/analytics-browser/CHANGELOG.md b/packages/analytics-browser/CHANGELOG.md index 774ddc1a2..2246a7d75 100644 --- a/packages/analytics-browser/CHANGELOG.md +++ b/packages/analytics-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.10.8](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.10.7...@amplitude/analytics-browser@1.10.8) (2023-06-08) + +**Note:** Version bump only for package @amplitude/analytics-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.10.7](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.10.6...@amplitude/analytics-browser@1.10.7) (2023-06-06) ### Bug Fixes diff --git a/packages/analytics-browser/README.md b/packages/analytics-browser/README.md index ce7fef47c..561338585 100644 --- a/packages/analytics-browser/README.md +++ b/packages/analytics-browser/README.md @@ -40,7 +40,7 @@ Alternatively, the package is also distributed through a CDN. Copy and paste the ```html diff --git a/packages/analytics-browser/generated/amplitude-snippet.js b/packages/analytics-browser/generated/amplitude-snippet.js index ea820219a..9b0323c79 100644 --- a/packages/analytics-browser/generated/amplitude-snippet.js +++ b/packages/analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-1RCGOEWBysSvMhkUFt9WiJ1yWQmI9CK/Epcmvz6EIhnaXEelKj6DQyduxW13vrki'; + as.integrity = 'sha384-GmHPBAFv1x8jEuQZmkqX6H0TvWj/UIZMKLZuB14iqzEdO5Ei6x+X5tmMCqiuzJ0+'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.10.7-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.10.8-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/analytics-browser/package.json b/packages/analytics-browser/package.json index 015831d5b..9025819b5 100644 --- a/packages/analytics-browser/package.json +++ b/packages/analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser", - "version": "1.10.7", + "version": "1.10.8", "description": "Official Amplitude SDK for Web", "keywords": [ "analytics", @@ -43,11 +43,11 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.0", - "@amplitude/analytics-core": "^1.0.0", - "@amplitude/analytics-types": "^1.0.0", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.1", - "@amplitude/plugin-web-attribution-browser": "^1.0.1", + "@amplitude/analytics-client-common": "^1.0.1", + "@amplitude/analytics-core": "^1.1.0", + "@amplitude/analytics-types": "^1.1.0", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.2", + "@amplitude/plugin-web-attribution-browser": "^1.0.2", "@amplitude/ua-parser-js": "^0.7.31", "tslib": "^2.4.1" }, diff --git a/packages/analytics-browser/playground/amplitude.js b/packages/analytics-browser/playground/amplitude.js index 1d15830bb..187784e39 100644 --- a/packages/analytics-browser/playground/amplitude.js +++ b/packages/analytics-browser/playground/amplitude.js @@ -1 +1 @@ -!function(){"use strict";var e=function(t,i){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])},e(t,i)};function t(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=t}e(t,i),t.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var i=function(){return i=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!V(t,i))return!1}return!0},V=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&M(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return M(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},z=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||V(t,i)))},e}(),F=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},B=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},$=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return B(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),Q="Event rejected due to exceeded retry count",K=function(e){return{promise:e||Promise.resolve()}},H=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new $(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return K(this.dispatch(r))},e.prototype.identify=function(e,t){var i=F(e,t);return K(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return K(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new z;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return K(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return K(this.dispatch(n))},e.prototype.add=function(e){return this.config?K(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),K())},e.prototype.remove=function(e){return this.config?K(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),K())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(B(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,B(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=B(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return K(this.timeline.flush())},e}(),W=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return M(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),Z="Amplitude Logger ",G=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=ee(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:if(null===(s=o.sent()))return this.fulfillRequest(e,0,"Unexpected error occurred"),[2];if(!t){if("body"in s){a="";try{a=JSON.stringify(s.body,null,2)}catch(e){}this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(a))}else this.fulfillRequest(e,s.statusCode,s.status);return[2]}return this.handleReponse(s,e),[3,4];case 3:return u=o.sent(),this.fulfillRequest(e,0,String(u)),[3,4];case 4:return[2]}}))}))},e.prototype.handleReponse=function(e,t){switch(e.status){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.handleOtherReponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherReponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(B(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ie=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},ne=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},re=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},oe=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=re(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},se=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ae)},ue=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),ce=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),le=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[C,t,e.substring(0,i)].filter(Boolean).join("_")},de=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),pe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(ce),fe="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},ve={exports:{}};q=ve,A=ve.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",D="Xiaomi",C="Zebra",j="Facebook",L=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},B=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?z(e,275):e,this},this.setUA(f),this};K.VERSION="0.7.31",K.BROWSER=L([a,l,"major"]),K.CPU=L([d]),K.DEVICE=L([s,c,u,p,f,h,v,g,b]),K.ENGINE=K.OS=L([a,l]),q.exports&&(A=q.exports=K),A.UAParser=K;var H=typeof e!==n&&(e.jQuery||e.Zepto);if(H&&!H.ua){var W=new K;H.ua=W.getResult(),H.ua.get=function(){return W.getUA()},H.ua.set=function(e){W.setUA(e);var t=W.getResult();for(var i in t)H.ua[i]=t[i]}}}("object"==typeof window?window:fe);var he=ve.exports,ge=function(){function e(){this.ua=new ve.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:me(),platform:"Web",os:be(this.ua),deviceModel:ye(this.ua)}},e}(),be=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},ye=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},me=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},we=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),_e=function(){return _e=Object.assign||function(e){for(var t,i=1,n=arguments.length;ie.sessionTimeout&&(t.setSessionId(n),i.session_id=t.getSessionId(),i.time=n),e.lastEventTime=n,[2,i])}))}))}}},bt={page_domain:"".concat(tt," Page Domain"),page_location:"".concat(tt," Page Location"),page_path:"".concat(tt," Page Path"),page_title:"".concat(tt," Page Title"),page_url:"".concat(tt," Page URL")},yt=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.init=function(e,t,n){return void 0===e&&(e=""),K(this._init(i(i({},n),{userId:t,apiKey:e})))},n.prototype._init=function(t){var n,s,u,c,l,d,p,v,h,g,b,m,w,_,I,k,E,S;return r(this,void 0,void 0,(function(){var T,O,x,P,R,N,q,A,D,C,j,L,M,V,z,F,B,$,Q=this;return o(this,(function(K){switch(K.label){case 0:return this.initializing?[2]:(this.initializing=!0,T=t,t.disableCookies?(O="",[3,5]):[3,1]);case 1:return null===(n=t.domain)||void 0===n?[3,2]:(x=n,[3,4]);case 2:return[4,r(void 0,void 0,void 0,(function(){var e,t,i,n,r,s,a;return o(this,(function(o){switch(o.label){case 0:return[4,(new de).isEnabled()];case 1:if(!o.sent()||!W&&"undefined"==typeof location)return[2,""];for(e=null!=W?W:location.hostname,t=e.split("."),i=[],n="AMP_TLDTEST",r=t.length-2;r>=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(Date.now()),z=!0),(F=Te()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new te).promise];case 11:return K.sent(),[4,this.add(new Ue).promise];case 12:return K.sent(),[4,this.add(gt()).promise];case 13:return K.sent(),[4,this.add(new Oe).promise];case 14:return K.sent(),("boolean"==typeof(H=this.config.defaultTracking)?H:null==H?void 0:H.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(ot,((n={})[ut]=o,n[ct]=i.pathname,n[lt]=e.id,n[dt]=e.text,n[pt]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,16];case 15:K.sent(),K.label=16;case 16:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(nt,((n={})[ft]=e.id,n[vt]=e.name,n[ht]=e.action,n)),t.track(rt,((r={})[ft]=e.id,r[vt]=e.name,r[ht]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,18];case 17:K.sent(),K.label=18;case 18:return(null===(_=this.config.attribution)||void 0===_?void 0:_.disabled)?[3,20]:(B=function(){for(var e,t,n=this,s=[],u=0;u0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!L(t,i))return!1}return!0},L=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&j(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return j(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},M=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||L(t,i)))},e}(),V=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},z=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},F=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return z(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),B="Event rejected due to exceeded retry count",$=function(e){return{promise:e||Promise.resolve()}},Q=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new F(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return $(this.dispatch(r))},e.prototype.identify=function(e,t){var i=V(e,t);return $(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return $(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new M;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return $(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return $(this.dispatch(n))},e.prototype.add=function(e){return this.config?$(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),$())},e.prototype.remove=function(e){return this.config?$(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),$())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(z(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,z(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=z(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return $(this.timeline.flush())},e}(),K=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return j(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),H="Amplitude Logger ",W=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=Y(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:return null===(s=o.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(s,e),[3,4]):("body"in s?this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(X(s))):this.fulfillRequest(e,s.statusCode,s.status),[2]);case 3:return a=o.sent(),u=(c=a)instanceof Error?c.message:String(c),this.config.loggerProvider.error(u),this.fulfillRequest(e,0,u),[3,4];case 4:return[2]}var c}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(X(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));d.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(z(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ne=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},re=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},oe=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},se=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=oe(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ae=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ue)},ce=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),le=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),de=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[A,t,e.substring(0,i)].filter(Boolean).join("_")},pe=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),fe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(le),ve="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},he={exports:{}};ee=he,te=he.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",q="Xiaomi",A="Zebra",D="Facebook",C=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},z=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?M(e,275):e,this},this.setUA(f),this};$.VERSION="0.7.31",$.BROWSER=C([a,l,"major"]),$.CPU=C([d]),$.DEVICE=C([s,c,u,p,f,h,v,g,b]),$.ENGINE=$.OS=C([a,l]),ee.exports&&(te=ee.exports=$),te.UAParser=$;var Q=typeof e!==n&&(e.jQuery||e.Zepto);if(Q&&!Q.ua){var K=new $;Q.ua=K.getResult(),Q.ua.get=function(){return K.getUA()},Q.ua.set=function(e){K.setUA(e);var t=K.getResult();for(var i in t)Q.ua[i]=t[i]}}}("object"==typeof window?window:ve);var ge=he.exports,be=function(){function e(){this.ua=new he.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:we(),platform:"Web",os:ye(this.ua),deviceModel:me(this.ua)}},e}(),ye=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},me=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},we=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},_e=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),Ie=function(){return Ie=Object.assign||function(e){for(var t,i=1,n=arguments.length;ie.sessionTimeout&&(t.setSessionId(n),i.session_id=t.getSessionId(),i.time=n),e.lastEventTime=n,[2,i])}))}))}}},yt={page_domain:"".concat(it," Page Domain"),page_location:"".concat(it," Page Location"),page_path:"".concat(it," Page Path"),page_title:"".concat(it," Page Title"),page_url:"".concat(it," Page URL")},mt=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.init=function(e,t,n){return void 0===e&&(e=""),$(this._init(i(i({},n),{userId:t,apiKey:e})))},n.prototype._init=function(t){var n,s,u,c,l,d,p,v,h,g,b,m,w,_,I,k,E,S;return r(this,void 0,void 0,(function(){var T,O,x,P,R,N,q,A,D,C,j,L,M,V,z,F,B,$,Q=this;return o(this,(function(K){switch(K.label){case 0:return this.initializing?[2]:(this.initializing=!0,T=t,t.disableCookies?(O="",[3,5]):[3,1]);case 1:return null===(n=t.domain)||void 0===n?[3,2]:(x=n,[3,4]);case 2:return[4,r(void 0,void 0,void 0,(function(){var e,t,i,n,r,s,a;return o(this,(function(o){switch(o.label){case 0:return[4,(new pe).isEnabled()];case 1:if(!o.sent()||!W&&"undefined"==typeof location)return[2,""];for(e=null!=W?W:location.hostname,t=e.split("."),i=[],n="AMP_TLDTEST",r=t.length-2;r>=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(Date.now()),z=!0),(F=Oe()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new ie).promise];case 11:return K.sent(),[4,this.add(new qe).promise];case 12:return K.sent(),[4,this.add(bt()).promise];case 13:return K.sent(),[4,this.add(new xe).promise];case 14:return K.sent(),("boolean"==typeof(H=this.config.defaultTracking)?H:null==H?void 0:H.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(st,((n={})[ct]=o,n[lt]=i.pathname,n[dt]=e.id,n[pt]=e.text,n[ft]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,16];case 15:K.sent(),K.label=16;case 16:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),t.track(ot,((r={})[vt]=e.id,r[ht]=e.name,r[gt]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,18];case 17:K.sent(),K.label=18;case 18:return(null===(_=this.config.attribution)||void 0===_?void 0:_.disabled)?[3,20]:(B=function(){for(var e,t,n=this,s=[],u=0;u ```html diff --git a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js index 513249106..012bd5255 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-mE2nuVUBrzajX9YbPZ8uOzONHjss0X7Z2RNoREFg60vDNS3SUPtSxZclXOIXu34H'; + as.integrity = 'sha384-esLjYq8Qgovxmz722pqEH0scVGMblCA2xQbwSRjQbtrbv5Q2RNz2ZfZkxd/G14eo'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.1-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.2-min.js.gz'; as.onload = function () { if (!window.amplitudeGTM.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/generated/amplitude-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-snippet.js index f8911227d..cc68ccb86 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-U6E9CMsOG+jKnR35qw+9MKntJySFcpJWIl3ultGC94Bq51pzfLpEavF79VTqG5iU'; + as.integrity = 'sha384-rGGnTWnBLdyeMUzGdN6uTRsJp8WDl5k35FXoetLOSqh4UVaZbxK09sSW/Mzp6tzZ'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.1-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.2-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/package.json b/packages/marketing-analytics-browser/package.json index bcec4c930..9d2e83812 100644 --- a/packages/marketing-analytics-browser/package.json +++ b/packages/marketing-analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/marketing-analytics-browser", - "version": "1.0.1", + "version": "1.0.2", "description": "Official Amplitude SDK for Web and Marketing Analytics", "keywords": [ "analytics", @@ -42,12 +42,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.10.7", - "@amplitude/analytics-client-common": "^1.0.0", - "@amplitude/analytics-core": "^1.0.0", - "@amplitude/analytics-types": "^1.0.0", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.1", - "@amplitude/plugin-web-attribution-browser": "^1.0.1", + "@amplitude/analytics-browser": "^1.10.8", + "@amplitude/analytics-client-common": "^1.0.1", + "@amplitude/analytics-core": "^1.1.0", + "@amplitude/analytics-types": "^1.1.0", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.2", + "@amplitude/plugin-web-attribution-browser": "^1.0.2", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/marketing-analytics-browser/src/version.ts b/packages/marketing-analytics-browser/src/version.ts index e96d2876d..926d3f2b3 100644 --- a/packages/marketing-analytics-browser/src/version.ts +++ b/packages/marketing-analytics-browser/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.0.1'; +export const VERSION = '1.0.2'; diff --git a/packages/plugin-page-view-tracking-browser/CHANGELOG.md b/packages/plugin-page-view-tracking-browser/CHANGELOG.md index 7f184b2bd..576865c52 100644 --- a/packages/plugin-page-view-tracking-browser/CHANGELOG.md +++ b/packages/plugin-page-view-tracking-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.1...@amplitude/plugin-page-view-tracking-browser@1.0.2) (2023-06-08) + +**Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.0...@amplitude/plugin-page-view-tracking-browser@1.0.1) (2023-06-06) **Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser diff --git a/packages/plugin-page-view-tracking-browser/package.json b/packages/plugin-page-view-tracking-browser/package.json index 9742188db..cf27c8fc0 100644 --- a/packages/plugin-page-view-tracking-browser/package.json +++ b/packages/plugin-page-view-tracking-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-page-view-tracking-browser", - "version": "1.0.1", + "version": "1.0.2", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -36,12 +36,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.0", - "@amplitude/analytics-types": "^1.0.0", + "@amplitude/analytics-client-common": "^1.0.1", + "@amplitude/analytics-types": "^1.1.0", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.10.7", + "@amplitude/analytics-browser": "^1.10.8", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", diff --git a/packages/plugin-web-attribution-browser/CHANGELOG.md b/packages/plugin-web-attribution-browser/CHANGELOG.md index 556c52fbf..354b623ce 100644 --- a/packages/plugin-web-attribution-browser/CHANGELOG.md +++ b/packages/plugin-web-attribution-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.1...@amplitude/plugin-web-attribution-browser@1.0.2) (2023-06-08) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.0...@amplitude/plugin-web-attribution-browser@1.0.1) (2023-06-06) **Note:** Version bump only for package @amplitude/plugin-web-attribution-browser diff --git a/packages/plugin-web-attribution-browser/package.json b/packages/plugin-web-attribution-browser/package.json index d00fb2b68..2dd91be9b 100644 --- a/packages/plugin-web-attribution-browser/package.json +++ b/packages/plugin-web-attribution-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-web-attribution-browser", - "version": "1.0.1", + "version": "1.0.2", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -36,12 +36,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.0", - "@amplitude/analytics-types": "^1.0.0", + "@amplitude/analytics-client-common": "^1.0.1", + "@amplitude/analytics-types": "^1.1.0", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.10.7", + "@amplitude/analytics-browser": "^1.10.8", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", From c6c82bae762493a07922e08baca5db1bd91e483e Mon Sep 17 00:00:00 2001 From: Justin Fiedler Date: Mon, 12 Jun 2023 15:48:41 -0700 Subject: [PATCH 011/214] feat: added extendSession() method to Browser Client (#425) * feat: added extendSession() method to Browser Client * fix: implement extendSession() method for RN Client and Browser Client factory * chore: code clean up * chore: fix mocks * chore: handle deferred config * chore: fix comment in test * chore: add snippet proxy test for extendSession() --- .../src/browser-client-factory.ts | 6 ++ .../analytics-browser/src/browser-client.ts | 8 ++ packages/analytics-browser/src/index.ts | 1 + .../test/browser-client.test.ts | 86 +++++++++++++++++++ .../analytics-browser/test/helpers/mock.ts | 1 + packages/analytics-browser/test/index.test.ts | 2 + .../src/react-native-client.ts | 14 +++ .../analytics-types/src/client/web-client.ts | 14 ++- 8 files changed, 131 insertions(+), 1 deletion(-) diff --git a/packages/analytics-browser/src/browser-client-factory.ts b/packages/analytics-browser/src/browser-client-factory.ts index b0c4efe42..bf8c4744e 100644 --- a/packages/analytics-browser/src/browser-client-factory.ts +++ b/packages/analytics-browser/src/browser-client-factory.ts @@ -107,6 +107,12 @@ export const createInstance = (): BrowserClient => { getClientLogConfig(client), getClientStates(client, ['config']), ), + extendSession: debugWrapper( + client.extendSession.bind(client), + 'extendSession', + getClientLogConfig(client), + getClientStates(client, ['config']), + ), setOptOut: debugWrapper( client.setOptOut.bind(client), 'setOptOut', diff --git a/packages/analytics-browser/src/browser-client.ts b/packages/analytics-browser/src/browser-client.ts index 8547a036b..7027847cc 100644 --- a/packages/analytics-browser/src/browser-client.ts +++ b/packages/analytics-browser/src/browser-client.ts @@ -221,6 +221,14 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { } } + extendSession() { + if (!this.config) { + this.q.push(this.extendSession.bind(this)); + return; + } + this.config.lastEventTime = Date.now(); + } + setTransport(transport: TransportType) { if (!this.config) { this.q.push(this.setTransport.bind(this, transport)); diff --git a/packages/analytics-browser/src/index.ts b/packages/analytics-browser/src/index.ts index 6f48549a5..b2cba6f06 100644 --- a/packages/analytics-browser/src/index.ts +++ b/packages/analytics-browser/src/index.ts @@ -3,6 +3,7 @@ import client from './browser-client-factory'; export { createInstance } from './browser-client-factory'; export const { add, + extendSession, flush, getDeviceId, getSessionId, diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index ff77c7879..78615b89d 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -551,6 +551,92 @@ describe('browser-client', () => { }); }); + describe('extendSession', () => { + test('should extend the current session without sending events', async () => { + const firstSessionId = 1; + const client = new AmplitudeBrowser(); + await client.init(API_KEY, undefined, { + sessionTimeout: 20, + sessionId: firstSessionId, + flushQueueSize: 1, + flushIntervalMillis: 1, + }).promise; + // assert sessionId is set + expect(client.config.sessionId).toBe(firstSessionId); + expect(client.config.lastEventTime).toBeUndefined(); + + // send an event + await client.track('test 1').promise; + const eventTime1 = client.config.lastEventTime ?? -1; + expect(eventTime1 > 0).toBeTruthy(); + + // wait for session to almost expire, then extend it + await new Promise((resolve) => + setTimeout(() => { + client.extendSession(); + resolve(); + }, 15), + ); + + // assert session id is unchanged + expect(client.config.sessionId).toBe(firstSessionId); + // assert last event time was updated + const extendedLastEventTime = client.config.lastEventTime ?? -1; + expect(extendedLastEventTime > 0).toBeTruthy(); + expect(extendedLastEventTime > eventTime1).toBeTruthy(); + + // send another event just before session expires (again) + await new Promise((resolve) => + // eslint-disable-next-line @typescript-eslint/no-misused-promises + setTimeout(async () => { + await client.track('test 2').promise; + resolve(); + }, 15), + ); + + // assert session id is unchanged + expect(client.config.sessionId).toBe(firstSessionId); + // assert last event time was updated + const eventTime2 = client.config.lastEventTime ?? -1; + expect(eventTime2 > 0).toBeTruthy(); + expect(eventTime2 > extendedLastEventTime).toBeTruthy(); + + // Wait for session to timeout, without extendSession() + await new Promise((resolve) => + // eslint-disable-next-line @typescript-eslint/no-misused-promises + setTimeout(async () => { + await client.track('test 3').promise; + resolve(); + }, 21), + ); + // assert session id is changed + expect(client.config.sessionId).not.toBe(firstSessionId); + expect(client.config.sessionId ?? -1 > firstSessionId).toBeTruthy(); + }); + + test('should extendSession using proxy', async () => { + const firstSessionId = 1; + const client = new AmplitudeBrowser(); + + // call extendSession() before init() + client.extendSession(); + + // init + await client.init(API_KEY, undefined, { + sessionTimeout: 20, + sessionId: firstSessionId, + flushQueueSize: 1, + flushIntervalMillis: 1, + lastEventTime: 0, + }).promise; + + // assert sessionId is unchanged + expect(client.config.sessionId).toBe(firstSessionId); + // assert last event time was updated + expect(client.config.lastEventTime).not.toBe(0); + }); + }); + describe('setTransport', () => { test('should set transport', async () => { const fetch = new FetchTransport(); diff --git a/packages/analytics-browser/test/helpers/mock.ts b/packages/analytics-browser/test/helpers/mock.ts index 1a55aef7d..d46704970 100644 --- a/packages/analytics-browser/test/helpers/mock.ts +++ b/packages/analytics-browser/test/helpers/mock.ts @@ -19,6 +19,7 @@ export const createAmplitudeMock = (): jest.MockedObject => ({ setDeviceId: jest.fn(), getSessionId: jest.fn(), setSessionId: jest.fn(), + extendSession: jest.fn(), reset: jest.fn(), setTransport: jest.fn(), }); diff --git a/packages/analytics-browser/test/index.test.ts b/packages/analytics-browser/test/index.test.ts index 894f3819a..b0090c54c 100644 --- a/packages/analytics-browser/test/index.test.ts +++ b/packages/analytics-browser/test/index.test.ts @@ -1,6 +1,7 @@ import { add, createInstance, + extendSession, flush, getDeviceId, getSessionId, @@ -28,6 +29,7 @@ describe('index', () => { test('should expose apis', () => { expect(typeof add).toBe('function'); expect(typeof createInstance).toBe('function'); + expect(typeof extendSession).toBe('function'); expect(typeof flush).toBe('function'); expect(typeof groupIdentify).toBe('function'); expect(typeof getDeviceId).toBe('function'); diff --git a/packages/analytics-react-native/src/react-native-client.ts b/packages/analytics-react-native/src/react-native-client.ts index 46ccd4316..d10f72d31 100644 --- a/packages/analytics-react-native/src/react-native-client.ts +++ b/packages/analytics-react-native/src/react-native-client.ts @@ -183,6 +183,14 @@ export class AmplitudeReactNative extends AmplitudeCore { void this.setSessionIdInternal(sessionId, this.currentTimeMillis()); } + extendSession() { + if (!this.config) { + this.q.push(this.extendSession.bind(this)); + return; + } + this.config.lastEventTime = this.currentTimeMillis(); + } + private setSessionIdInternal(sessionId: number, eventTime: number) { const previousSessionId = this.config.sessionId; if (previousSessionId === sessionId) { @@ -387,6 +395,12 @@ export const createInstance = (): ReactNativeClient => { getClientLogConfig(client), getClientStates(client, ['config']), ), + extendSession: debugWrapper( + client.extendSession.bind(client), + 'extendSession', + getClientLogConfig(client), + getClientStates(client, ['config']), + ), setOptOut: debugWrapper( client.setOptOut.bind(client), 'setOptOut', diff --git a/packages/analytics-types/src/client/web-client.ts b/packages/analytics-types/src/client/web-client.ts index 5d09e40ef..731678bf0 100644 --- a/packages/analytics-types/src/client/web-client.ts +++ b/packages/analytics-types/src/client/web-client.ts @@ -53,7 +53,7 @@ interface Client extends CoreClient { /** * Sets a new session ID. - * When settign a custom session ID, make sure the value is in milliseconds since epoch (Unix Timestamp). + * When setting a custom session ID, make sure the value is in milliseconds since epoch (Unix Timestamp). * * ```typescript * setSessionId(Date.now()); @@ -61,6 +61,18 @@ interface Client extends CoreClient { */ setSessionId(sessionId: number): void; + /** + * Extends the current session (advanced) + * + * Normally sessions are extended automatically by track()'ing events. If you want to extend the session without + * tracking and event, this will set the last user interaction to the current time. + * + * ```typescript + * extendSession(); + * ``` + */ + extendSession(): void; + /** * Anonymizes users after they log out, by: * From 5cfe645df26d9dc301f264ead0880dad6925a2ff Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Tue, 13 Jun 2023 00:04:17 +0000 Subject: [PATCH 012/214] chore(release): publish - @amplitude/analytics-browser-test@1.4.3 - @amplitude/analytics-browser@1.11.0 - @amplitude/analytics-client-common@1.0.2 - @amplitude/analytics-core@1.1.1 - @amplitude/analytics-node-test@1.0.2 - @amplitude/analytics-node@1.2.1 - @amplitude/analytics-react-native@1.2.0 - @amplitude/analytics-types@1.2.0 - @amplitude/marketing-analytics-browser@1.0.3 - @amplitude/plugin-page-view-tracking-browser@1.0.3 - @amplitude/plugin-web-attribution-browser@1.0.3 --- packages/analytics-browser-test/CHANGELOG.md | 9 +++++++++ packages/analytics-browser-test/package.json | 4 ++-- packages/analytics-browser/CHANGELOG.md | 12 ++++++++++++ packages/analytics-browser/README.md | 2 +- .../generated/amplitude-snippet.js | 4 ++-- packages/analytics-browser/package.json | 12 ++++++------ packages/analytics-browser/playground/amplitude.js | 2 +- packages/analytics-browser/src/version.ts | 2 +- packages/analytics-client-common/CHANGELOG.md | 9 +++++++++ packages/analytics-client-common/package.json | 6 +++--- packages/analytics-core/CHANGELOG.md | 9 +++++++++ packages/analytics-core/package.json | 4 ++-- packages/analytics-node-test/CHANGELOG.md | 9 +++++++++ packages/analytics-node-test/package.json | 4 ++-- packages/analytics-node/CHANGELOG.md | 9 +++++++++ packages/analytics-node/package.json | 6 +++--- packages/analytics-node/src/version.ts | 2 +- packages/analytics-react-native/CHANGELOG.md | 12 ++++++++++++ packages/analytics-react-native/package.json | 8 ++++---- packages/analytics-react-native/src/version.ts | 2 +- packages/analytics-types/CHANGELOG.md | 12 ++++++++++++ packages/analytics-types/package.json | 2 +- packages/marketing-analytics-browser/CHANGELOG.md | 9 +++++++++ packages/marketing-analytics-browser/README.md | 2 +- .../generated/amplitude-gtm-snippet.js | 4 ++-- .../generated/amplitude-snippet.js | 4 ++-- packages/marketing-analytics-browser/package.json | 14 +++++++------- .../marketing-analytics-browser/src/version.ts | 2 +- .../plugin-page-view-tracking-browser/CHANGELOG.md | 9 +++++++++ .../plugin-page-view-tracking-browser/package.json | 8 ++++---- .../plugin-web-attribution-browser/CHANGELOG.md | 9 +++++++++ .../plugin-web-attribution-browser/package.json | 8 ++++---- 32 files changed, 159 insertions(+), 51 deletions(-) diff --git a/packages/analytics-browser-test/CHANGELOG.md b/packages/analytics-browser-test/CHANGELOG.md index ebf9e1d7c..ff4138537 100644 --- a/packages/analytics-browser-test/CHANGELOG.md +++ b/packages/analytics-browser-test/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.4.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.2...@amplitude/analytics-browser-test@1.4.3) (2023-06-13) + +**Note:** Version bump only for package @amplitude/analytics-browser-test + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.4.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.1...@amplitude/analytics-browser-test@1.4.2) (2023-06-08) **Note:** Version bump only for package @amplitude/analytics-browser-test diff --git a/packages/analytics-browser-test/package.json b/packages/analytics-browser-test/package.json index c20f27036..7761cb358 100644 --- a/packages/analytics-browser-test/package.json +++ b/packages/analytics-browser-test/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser-test", - "version": "1.4.2", + "version": "1.4.3", "private": true, "description": "", "author": "Amplitude Inc", @@ -16,7 +16,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.10.8" + "@amplitude/analytics-browser": "^1.11.0" }, "devDependencies": { "nock": "^13.2.4" diff --git a/packages/analytics-browser/CHANGELOG.md b/packages/analytics-browser/CHANGELOG.md index 2246a7d75..bbb729bb4 100644 --- a/packages/analytics-browser/CHANGELOG.md +++ b/packages/analytics-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.11.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.10.8...@amplitude/analytics-browser@1.11.0) (2023-06-13) + +### Features + +- added extendSession() method to Browser Client ([#425](https://github.com/amplitude/Amplitude-TypeScript/issues/425)) + ([c6c82ba](https://github.com/amplitude/Amplitude-TypeScript/commit/c6c82bae762493a07922e08baca5db1bd91e483e)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.10.8](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.10.7...@amplitude/analytics-browser@1.10.8) (2023-06-08) **Note:** Version bump only for package @amplitude/analytics-browser diff --git a/packages/analytics-browser/README.md b/packages/analytics-browser/README.md index 561338585..b39f0da86 100644 --- a/packages/analytics-browser/README.md +++ b/packages/analytics-browser/README.md @@ -40,7 +40,7 @@ Alternatively, the package is also distributed through a CDN. Copy and paste the ```html diff --git a/packages/analytics-browser/generated/amplitude-snippet.js b/packages/analytics-browser/generated/amplitude-snippet.js index 9b0323c79..ee6a95c3b 100644 --- a/packages/analytics-browser/generated/amplitude-snippet.js +++ b/packages/analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-GmHPBAFv1x8jEuQZmkqX6H0TvWj/UIZMKLZuB14iqzEdO5Ei6x+X5tmMCqiuzJ0+'; + as.integrity = 'sha384-88G7ldPwlcBAEMvub2fJXyrErbB+r+yi8fkKzYrr3iUDAkNi/0Y6kR2jyCMC1rfJ'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.10.8-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.11.0-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/analytics-browser/package.json b/packages/analytics-browser/package.json index 9025819b5..73ae2af67 100644 --- a/packages/analytics-browser/package.json +++ b/packages/analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser", - "version": "1.10.8", + "version": "1.11.0", "description": "Official Amplitude SDK for Web", "keywords": [ "analytics", @@ -43,11 +43,11 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.1", - "@amplitude/analytics-core": "^1.1.0", - "@amplitude/analytics-types": "^1.1.0", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.2", - "@amplitude/plugin-web-attribution-browser": "^1.0.2", + "@amplitude/analytics-client-common": "^1.0.2", + "@amplitude/analytics-core": "^1.1.1", + "@amplitude/analytics-types": "^1.2.0", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.3", + "@amplitude/plugin-web-attribution-browser": "^1.0.3", "@amplitude/ua-parser-js": "^0.7.31", "tslib": "^2.4.1" }, diff --git a/packages/analytics-browser/playground/amplitude.js b/packages/analytics-browser/playground/amplitude.js index 187784e39..7f02b4256 100644 --- a/packages/analytics-browser/playground/amplitude.js +++ b/packages/analytics-browser/playground/amplitude.js @@ -1 +1 @@ -!function(){"use strict";var e=function(t,i){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])},e(t,i)};function t(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=t}e(t,i),t.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var i=function(){return i=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!L(t,i))return!1}return!0},L=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&j(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return j(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},M=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||L(t,i)))},e}(),V=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},z=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},F=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return z(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),B="Event rejected due to exceeded retry count",$=function(e){return{promise:e||Promise.resolve()}},Q=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new F(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return $(this.dispatch(r))},e.prototype.identify=function(e,t){var i=V(e,t);return $(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return $(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new M;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return $(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return $(this.dispatch(n))},e.prototype.add=function(e){return this.config?$(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),$())},e.prototype.remove=function(e){return this.config?$(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),$())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(z(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,z(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=z(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return $(this.timeline.flush())},e}(),K=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return j(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),H="Amplitude Logger ",W=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=Y(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:return null===(s=o.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(s,e),[3,4]):("body"in s?this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(X(s))):this.fulfillRequest(e,s.statusCode,s.status),[2]);case 3:return a=o.sent(),u=(c=a)instanceof Error?c.message:String(c),this.config.loggerProvider.error(u),this.fulfillRequest(e,0,u),[3,4];case 4:return[2]}var c}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(X(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));d.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(z(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ne=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},re=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},oe=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},se=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=oe(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ae=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ue)},ce=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),le=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),de=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[A,t,e.substring(0,i)].filter(Boolean).join("_")},pe=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),fe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(le),ve="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},he={exports:{}};ee=he,te=he.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",q="Xiaomi",A="Zebra",D="Facebook",C=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},z=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?M(e,275):e,this},this.setUA(f),this};$.VERSION="0.7.31",$.BROWSER=C([a,l,"major"]),$.CPU=C([d]),$.DEVICE=C([s,c,u,p,f,h,v,g,b]),$.ENGINE=$.OS=C([a,l]),ee.exports&&(te=ee.exports=$),te.UAParser=$;var Q=typeof e!==n&&(e.jQuery||e.Zepto);if(Q&&!Q.ua){var K=new $;Q.ua=K.getResult(),Q.ua.get=function(){return K.getUA()},Q.ua.set=function(e){K.setUA(e);var t=K.getResult();for(var i in t)Q.ua[i]=t[i]}}}("object"==typeof window?window:ve);var ge=he.exports,be=function(){function e(){this.ua=new he.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:we(),platform:"Web",os:ye(this.ua),deviceModel:me(this.ua)}},e}(),ye=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},me=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},we=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},_e=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),Ie=function(){return Ie=Object.assign||function(e){for(var t,i=1,n=arguments.length;ie.sessionTimeout&&(t.setSessionId(n),i.session_id=t.getSessionId(),i.time=n),e.lastEventTime=n,[2,i])}))}))}}},yt={page_domain:"".concat(it," Page Domain"),page_location:"".concat(it," Page Location"),page_path:"".concat(it," Page Path"),page_title:"".concat(it," Page Title"),page_url:"".concat(it," Page URL")},mt=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.init=function(e,t,n){return void 0===e&&(e=""),$(this._init(i(i({},n),{userId:t,apiKey:e})))},n.prototype._init=function(t){var n,s,u,c,l,d,p,v,h,g,b,m,w,_,I,k,E,S;return r(this,void 0,void 0,(function(){var T,O,x,P,R,N,q,A,D,C,j,L,M,V,z,F,B,$,Q=this;return o(this,(function(K){switch(K.label){case 0:return this.initializing?[2]:(this.initializing=!0,T=t,t.disableCookies?(O="",[3,5]):[3,1]);case 1:return null===(n=t.domain)||void 0===n?[3,2]:(x=n,[3,4]);case 2:return[4,r(void 0,void 0,void 0,(function(){var e,t,i,n,r,s,a;return o(this,(function(o){switch(o.label){case 0:return[4,(new pe).isEnabled()];case 1:if(!o.sent()||!W&&"undefined"==typeof location)return[2,""];for(e=null!=W?W:location.hostname,t=e.split("."),i=[],n="AMP_TLDTEST",r=t.length-2;r>=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(Date.now()),z=!0),(F=Oe()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new ie).promise];case 11:return K.sent(),[4,this.add(new qe).promise];case 12:return K.sent(),[4,this.add(bt()).promise];case 13:return K.sent(),[4,this.add(new xe).promise];case 14:return K.sent(),("boolean"==typeof(H=this.config.defaultTracking)?H:null==H?void 0:H.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(st,((n={})[ct]=o,n[lt]=i.pathname,n[dt]=e.id,n[pt]=e.text,n[ft]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,16];case 15:K.sent(),K.label=16;case 16:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),t.track(ot,((r={})[vt]=e.id,r[ht]=e.name,r[gt]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,18];case 17:K.sent(),K.label=18;case 18:return(null===(_=this.config.attribution)||void 0===_?void 0:_.disabled)?[3,20]:(B=function(){for(var e,t,n=this,s=[],u=0;u0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!L(t,i))return!1}return!0},L=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&j(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return j(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},M=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||L(t,i)))},e}(),V=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},z=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},F=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return z(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),B="Event rejected due to exceeded retry count",$=function(e){return{promise:e||Promise.resolve()}},Q=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new F(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return $(this.dispatch(r))},e.prototype.identify=function(e,t){var i=V(e,t);return $(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return $(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new M;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return $(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return $(this.dispatch(n))},e.prototype.add=function(e){return this.config?$(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),$())},e.prototype.remove=function(e){return this.config?$(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),$())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(z(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,z(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=z(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return $(this.timeline.flush())},e}(),K=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return j(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),H="Amplitude Logger ",W=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=Y(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:return null===(s=o.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(s,e),[3,4]):("body"in s?this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(X(s))):this.fulfillRequest(e,s.statusCode,s.status),[2]);case 3:return a=o.sent(),u=(c=a)instanceof Error?c.message:String(c),this.config.loggerProvider.error(u),this.fulfillRequest(e,0,u),[3,4];case 4:return[2]}var c}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(X(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));d.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(z(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ne=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},re=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},oe=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},se=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=oe(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ae=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ue)},ce=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),le=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),de=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[A,t,e.substring(0,i)].filter(Boolean).join("_")},pe=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),fe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(le),ve="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},he={exports:{}};ee=he,te=he.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",q="Xiaomi",A="Zebra",D="Facebook",C=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},z=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?M(e,275):e,this},this.setUA(f),this};$.VERSION="0.7.31",$.BROWSER=C([a,l,"major"]),$.CPU=C([d]),$.DEVICE=C([s,c,u,p,f,h,v,g,b]),$.ENGINE=$.OS=C([a,l]),ee.exports&&(te=ee.exports=$),te.UAParser=$;var Q=typeof e!==n&&(e.jQuery||e.Zepto);if(Q&&!Q.ua){var K=new $;Q.ua=K.getResult(),Q.ua.get=function(){return K.getUA()},Q.ua.set=function(e){K.setUA(e);var t=K.getResult();for(var i in t)Q.ua[i]=t[i]}}}("object"==typeof window?window:ve);var ge=he.exports,be=function(){function e(){this.ua=new he.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:we(),platform:"Web",os:ye(this.ua),deviceModel:me(this.ua)}},e}(),ye=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},me=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},we=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},_e=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),Ie=function(){return Ie=Object.assign||function(e){for(var t,i=1,n=arguments.length;ie.sessionTimeout&&(t.setSessionId(n),i.session_id=t.getSessionId(),i.time=n),e.lastEventTime=n,[2,i])}))}))}}},yt={page_domain:"".concat(it," Page Domain"),page_location:"".concat(it," Page Location"),page_path:"".concat(it," Page Path"),page_title:"".concat(it," Page Title"),page_url:"".concat(it," Page URL")},mt=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.init=function(e,t,n){return void 0===e&&(e=""),$(this._init(i(i({},n),{userId:t,apiKey:e})))},n.prototype._init=function(t){var n,s,u,c,l,d,p,v,h,g,b,m,w,_,I,k,E,S;return r(this,void 0,void 0,(function(){var T,O,x,P,R,N,q,A,D,C,j,L,M,V,z,F,B,$,Q=this;return o(this,(function(K){switch(K.label){case 0:return this.initializing?[2]:(this.initializing=!0,T=t,t.disableCookies?(O="",[3,5]):[3,1]);case 1:return null===(n=t.domain)||void 0===n?[3,2]:(x=n,[3,4]);case 2:return[4,r(void 0,void 0,void 0,(function(){var e,t,i,n,r,s,a;return o(this,(function(o){switch(o.label){case 0:return[4,(new pe).isEnabled()];case 1:if(!o.sent()||!W&&"undefined"==typeof location)return[2,""];for(e=null!=W?W:location.hostname,t=e.split("."),i=[],n="AMP_TLDTEST",r=t.length-2;r>=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(Date.now()),z=!0),(F=Oe()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new ie).promise];case 11:return K.sent(),[4,this.add(new qe).promise];case 12:return K.sent(),[4,this.add(bt()).promise];case 13:return K.sent(),[4,this.add(new xe).promise];case 14:return K.sent(),("boolean"==typeof(H=this.config.defaultTracking)?H:null==H?void 0:H.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(st,((n={})[ct]=o,n[lt]=i.pathname,n[dt]=e.id,n[pt]=e.text,n[ft]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,16];case 15:K.sent(),K.label=16;case 16:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),t.track(ot,((r={})[vt]=e.id,r[ht]=e.name,r[gt]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,18];case 17:K.sent(),K.label=18;case 18:return(null===(_=this.config.attribution)||void 0===_?void 0:_.disabled)?[3,20]:(B=function(){for(var e,t,n=this,s=[],u=0;u ```html diff --git a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js index 012bd5255..f11bba734 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-esLjYq8Qgovxmz722pqEH0scVGMblCA2xQbwSRjQbtrbv5Q2RNz2ZfZkxd/G14eo'; + as.integrity = 'sha384-dqd1zhQwCaPNwnnxfckFnnSnDdhem8MpuxJOSB6Yvxc1uRXwkeyIsPXpUnbBIu4u'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.2-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.3-min.js.gz'; as.onload = function () { if (!window.amplitudeGTM.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/generated/amplitude-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-snippet.js index cc68ccb86..d416f439b 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-rGGnTWnBLdyeMUzGdN6uTRsJp8WDl5k35FXoetLOSqh4UVaZbxK09sSW/Mzp6tzZ'; + as.integrity = 'sha384-Bnx/ydv/ChaWFeFAQkQJzhK20h7Daqih3HYpGGtC0BFpXuhOLf794kHvO2p7YuZW'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.2-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.3-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/package.json b/packages/marketing-analytics-browser/package.json index 9d2e83812..b240dfe79 100644 --- a/packages/marketing-analytics-browser/package.json +++ b/packages/marketing-analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/marketing-analytics-browser", - "version": "1.0.2", + "version": "1.0.3", "description": "Official Amplitude SDK for Web and Marketing Analytics", "keywords": [ "analytics", @@ -42,12 +42,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.10.8", - "@amplitude/analytics-client-common": "^1.0.1", - "@amplitude/analytics-core": "^1.1.0", - "@amplitude/analytics-types": "^1.1.0", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.2", - "@amplitude/plugin-web-attribution-browser": "^1.0.2", + "@amplitude/analytics-browser": "^1.11.0", + "@amplitude/analytics-client-common": "^1.0.2", + "@amplitude/analytics-core": "^1.1.1", + "@amplitude/analytics-types": "^1.2.0", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.3", + "@amplitude/plugin-web-attribution-browser": "^1.0.3", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/marketing-analytics-browser/src/version.ts b/packages/marketing-analytics-browser/src/version.ts index 926d3f2b3..30cbd1ac5 100644 --- a/packages/marketing-analytics-browser/src/version.ts +++ b/packages/marketing-analytics-browser/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.0.2'; +export const VERSION = '1.0.3'; diff --git a/packages/plugin-page-view-tracking-browser/CHANGELOG.md b/packages/plugin-page-view-tracking-browser/CHANGELOG.md index 576865c52..a7c738e6f 100644 --- a/packages/plugin-page-view-tracking-browser/CHANGELOG.md +++ b/packages/plugin-page-view-tracking-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.2...@amplitude/plugin-page-view-tracking-browser@1.0.3) (2023-06-13) + +**Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.1...@amplitude/plugin-page-view-tracking-browser@1.0.2) (2023-06-08) **Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser diff --git a/packages/plugin-page-view-tracking-browser/package.json b/packages/plugin-page-view-tracking-browser/package.json index cf27c8fc0..3c9ab818f 100644 --- a/packages/plugin-page-view-tracking-browser/package.json +++ b/packages/plugin-page-view-tracking-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-page-view-tracking-browser", - "version": "1.0.2", + "version": "1.0.3", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -36,12 +36,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.1", - "@amplitude/analytics-types": "^1.1.0", + "@amplitude/analytics-client-common": "^1.0.2", + "@amplitude/analytics-types": "^1.2.0", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.10.8", + "@amplitude/analytics-browser": "^1.11.0", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", diff --git a/packages/plugin-web-attribution-browser/CHANGELOG.md b/packages/plugin-web-attribution-browser/CHANGELOG.md index 354b623ce..046ebe285 100644 --- a/packages/plugin-web-attribution-browser/CHANGELOG.md +++ b/packages/plugin-web-attribution-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.2...@amplitude/plugin-web-attribution-browser@1.0.3) (2023-06-13) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.1...@amplitude/plugin-web-attribution-browser@1.0.2) (2023-06-08) **Note:** Version bump only for package @amplitude/plugin-web-attribution-browser diff --git a/packages/plugin-web-attribution-browser/package.json b/packages/plugin-web-attribution-browser/package.json index 2dd91be9b..a4e4358ea 100644 --- a/packages/plugin-web-attribution-browser/package.json +++ b/packages/plugin-web-attribution-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-web-attribution-browser", - "version": "1.0.2", + "version": "1.0.3", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -36,12 +36,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.1", - "@amplitude/analytics-types": "^1.1.0", + "@amplitude/analytics-client-common": "^1.0.2", + "@amplitude/analytics-types": "^1.2.0", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.10.8", + "@amplitude/analytics-browser": "^1.11.0", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", From 52f7b08cc9501e41a8e421c7f84458dea78db972 Mon Sep 17 00:00:00 2001 From: Andrey Sokolov Date: Tue, 13 Jun 2023 20:39:05 +0400 Subject: [PATCH 013/214] fix: do not fetch advertising Id if adid tracking is disabled (#424) * fix: do not fetch advertising Id if adid tracking is disabled * fix: ios compilation --- .../reactnative/AmplitudeReactNativeModule.kt | 30 +++++++++++-------- .../reactnative/AndroidContextProvider.kt | 18 +++++++---- .../ios/AmplitudeReactNative.m | 2 +- .../ios/AmplitudeReactNative.swift | 3 +- .../src/plugins/context.ts | 4 +-- 5 files changed, 35 insertions(+), 22 deletions(-) diff --git a/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AmplitudeReactNativeModule.kt b/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AmplitudeReactNativeModule.kt index 6e8d4d13d..d42085c9c 100644 --- a/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AmplitudeReactNativeModule.kt +++ b/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AmplitudeReactNativeModule.kt @@ -13,25 +13,31 @@ const val MODULE_NAME = "AmplitudeReactNative" @ReactModule(name = MODULE_NAME) class AmplitudeReactNativeModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { - private val androidContextProvider = AndroidContextProvider(reactContext.applicationContext, false) + private var androidContextProvider: AndroidContextProvider? = null override fun getName(): String { return MODULE_NAME } @ReactMethod - private fun getApplicationContext(promise: Promise) { + private fun getApplicationContext(shouldTrackAdid: Boolean, promise: Promise) { + if (androidContextProvider == null) { + androidContextProvider = AndroidContextProvider(reactContext.applicationContext, false, shouldTrackAdid) + } + promise.resolve(WritableNativeMap().apply { - putString("version", androidContextProvider.versionName) - putString("platform", androidContextProvider.platform) - putString("language", androidContextProvider.language) - putString("osName", androidContextProvider.osName) - putString("osVersion", androidContextProvider.osVersion) - putString("deviceBrand", androidContextProvider.brand) - putString("deviceManufacturer", androidContextProvider.manufacturer) - putString("deviceModel", androidContextProvider.model) - putString("carrier", androidContextProvider.carrier) - putString("adid", androidContextProvider.advertisingId) + putString("version", androidContextProvider!!.versionName) + putString("platform", androidContextProvider!!.platform) + putString("language", androidContextProvider!!.language) + putString("osName", androidContextProvider!!.osName) + putString("osVersion", androidContextProvider!!.osVersion) + putString("deviceBrand", androidContextProvider!!.brand) + putString("deviceManufacturer", androidContextProvider!!.manufacturer) + putString("deviceModel", androidContextProvider!!.model) + putString("carrier", androidContextProvider!!.carrier) + if (androidContextProvider!!.advertisingId != null) { + putString("adid", androidContextProvider!!.advertisingId) + } }) } } diff --git a/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AndroidContextProvider.kt b/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AndroidContextProvider.kt index e414cb3cd..11297ad8a 100644 --- a/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AndroidContextProvider.kt +++ b/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AndroidContextProvider.kt @@ -20,8 +20,9 @@ import java.util.Locale import java.util.UUID import kotlin.collections.ArrayList -class AndroidContextProvider(private val context: Context, locationListening: Boolean) { +class AndroidContextProvider(private val context: Context, locationListening: Boolean, shouldTrackAdid: Boolean) { var isLocationListening = true + var shouldTrackAdid = true private var cachedInfo: CachedInfo? = null private get() { if (field == null) { @@ -34,7 +35,7 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo * Internal class serves as a cache */ inner class CachedInfo { - var advertisingId: String + var advertisingId: String? val country: String? val versionName: String? val osName: String @@ -201,7 +202,11 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo return locale.language } - private fun fetchAdvertisingId(): String { + private fun fetchAdvertisingId(): String? { + if (!shouldTrackAdid) { + return null + } + // This should not be called on the main thread. return if ("Amazon" == fetchManufacturer()) { fetchAndCacheAmazonAdvertisingId @@ -237,14 +242,14 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo return appSetId } - private val fetchAndCacheAmazonAdvertisingId: String + private val fetchAndCacheAmazonAdvertisingId: String? private get() { val cr = context.contentResolver limitAdTrackingEnabled = Secure.getInt(cr, SETTING_LIMIT_AD_TRACKING, 0) == 1 advertisingId = Secure.getString(cr, SETTING_ADVERTISING_ID) return advertisingId } - private val fetchAndCacheGoogleAdvertisingId: String + private val fetchAndCacheGoogleAdvertisingId: String? private get() { try { val AdvertisingIdClient = Class @@ -340,7 +345,7 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo get() = cachedInfo!!.country val language: String get() = cachedInfo!!.language - val advertisingId: String + val advertisingId: String? get() = cachedInfo!!.advertisingId val appSetId: String get() = cachedInfo!!.appSetId // other causes// failed to get providers list @@ -416,5 +421,6 @@ class AndroidContextProvider(private val context: Context, locationListening: Bo init { isLocationListening = locationListening + this.shouldTrackAdid = shouldTrackAdid } } diff --git a/packages/analytics-react-native/ios/AmplitudeReactNative.m b/packages/analytics-react-native/ios/AmplitudeReactNative.m index 1700bba9b..3c9a587a2 100644 --- a/packages/analytics-react-native/ios/AmplitudeReactNative.m +++ b/packages/analytics-react-native/ios/AmplitudeReactNative.m @@ -2,6 +2,6 @@ @interface RCT_EXTERN_MODULE(AmplitudeReactNative, NSObject) -RCT_EXTERN_METHOD(getApplicationContext: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(getApplicationContext: (BOOL)shouldTrackAdid resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) @end diff --git a/packages/analytics-react-native/ios/AmplitudeReactNative.swift b/packages/analytics-react-native/ios/AmplitudeReactNative.swift index 60432835d..d1d8d51a5 100644 --- a/packages/analytics-react-native/ios/AmplitudeReactNative.swift +++ b/packages/analytics-react-native/ios/AmplitudeReactNative.swift @@ -13,7 +13,8 @@ class ReactNative: NSObject { @objc func getApplicationContext( - _ resolve: RCTPromiseResolveBlock, + _ shouldTrackAdid: Bool, + resolver resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock ) -> Void { let applicationContext: [String: String?] = [ diff --git a/packages/analytics-react-native/src/plugins/context.ts b/packages/analytics-react-native/src/plugins/context.ts index 85db8b021..08391e616 100644 --- a/packages/analytics-react-native/src/plugins/context.ts +++ b/packages/analytics-react-native/src/plugins/context.ts @@ -22,7 +22,7 @@ type NativeContext = { }; export interface AmplitudeReactNative { - getApplicationContext(): Promise; + getApplicationContext(shouldTrackAdid: boolean): Promise; } export class Context implements BeforePlugin { @@ -55,7 +55,7 @@ export class Context implements BeforePlugin { async execute(context: Event): Promise { const time = new Date().getTime(); - const nativeContext = await this.nativeModule?.getApplicationContext(); + const nativeContext = await this.nativeModule?.getApplicationContext(this.config.trackingOptions.adid ?? false); const appVersion = nativeContext?.version || this.config.appVersion; const platform = nativeContext?.platform || BROWSER_PLATFORM; const osName = nativeContext?.osName || this.uaResult.browser.name; From 477399777aebd9a1a0a00dc0e11504399d0a4031 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Tue, 13 Jun 2023 16:53:37 +0000 Subject: [PATCH 014/214] chore(release): publish - @amplitude/analytics-react-native@1.2.1 --- packages/analytics-react-native/CHANGELOG.md | 13 +++++++++++++ packages/analytics-react-native/package.json | 2 +- packages/analytics-react-native/src/version.ts | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/analytics-react-native/CHANGELOG.md b/packages/analytics-react-native/CHANGELOG.md index e2ed696f5..364cb58fd 100644 --- a/packages/analytics-react-native/CHANGELOG.md +++ b/packages/analytics-react-native/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.2.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-react-native@1.2.0...@amplitude/analytics-react-native@1.2.1) (2023-06-13) + +### Bug Fixes + +- do not fetch advertising Id if adid tracking is disabled + ([#424](https://github.com/amplitude/Amplitude-TypeScript/issues/424)) + ([52f7b08](https://github.com/amplitude/Amplitude-TypeScript/commit/52f7b08cc9501e41a8e421c7f84458dea78db972)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [1.2.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-react-native@1.1.9...@amplitude/analytics-react-native@1.2.0) (2023-06-13) ### Features diff --git a/packages/analytics-react-native/package.json b/packages/analytics-react-native/package.json index 116448feb..9076767de 100644 --- a/packages/analytics-react-native/package.json +++ b/packages/analytics-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-react-native", - "version": "1.2.0", + "version": "1.2.1", "description": "Official React Native SDK", "keywords": [ "analytics", diff --git a/packages/analytics-react-native/src/version.ts b/packages/analytics-react-native/src/version.ts index 408a62f23..5ec4dffca 100644 --- a/packages/analytics-react-native/src/version.ts +++ b/packages/analytics-react-native/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.2.0'; +export const VERSION = '1.2.1'; From b46f2f4ff9cce2848bdd9bf554377ca7ed47983c Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Tue, 13 Jun 2023 18:13:56 -0700 Subject: [PATCH 015/214] fix: session end events being assigned to a different session id (#426) (#431) --- .../analytics-browser-test/test/index.test.ts | 408 ++++++++++-------- .../analytics-browser/src/browser-client.ts | 71 ++- .../src/cookie-migration/index.ts | 7 +- .../analytics-browser/src/plugins/context.ts | 13 +- .../src/plugins/session-handler.ts | 53 --- .../test/browser-client.test.ts | 73 +++- .../analytics-browser/test/config.test.ts | 3 + .../test/cookie-migration/index.test.ts | 29 +- .../test/plugins/context.test.ts | 11 +- .../test/plugins/session-handler.test.ts | 97 ----- 10 files changed, 388 insertions(+), 377 deletions(-) delete mode 100644 packages/analytics-browser/src/plugins/session-handler.ts delete mode 100644 packages/analytics-browser/test/plugins/session-handler.test.ts diff --git a/packages/analytics-browser-test/test/index.test.ts b/packages/analytics-browser-test/test/index.test.ts index 9b3072dbd..1354199be 100644 --- a/packages/analytics-browser-test/test/index.test.ts +++ b/packages/analytics-browser-test/test/index.test.ts @@ -6,7 +6,7 @@ import { default as nock } from 'nock'; import { success } from './responses'; import 'isomorphic-fetch'; import { path, SUCCESS_MESSAGE, url, uuidPattern } from './constants'; -import { PluginType, LogLevel, BaseEvent, Status } from '@amplitude/analytics-types'; +import { PluginType, LogLevel, Status } from '@amplitude/analytics-types'; import { UUID } from '@amplitude/analytics-core'; describe('integration', () => { @@ -20,6 +20,12 @@ describe('integration', () => { disabled: true, }, }; + const defaultTracking = { + pageViews: false, + sessions: false, + fileDownloads: false, + formInteractions: false, + }; let apiKey = ''; let client = amplitude.createInstance(); @@ -882,7 +888,25 @@ describe('integration', () => { }); describe('session handler', () => { - test('should send session events and replaced with known user', () => { + const previousSessionId = Date.now() - 31 * 60 * 1000; // now minus 31 minutes + const previousSessionLastEventTime = Date.now() - 31 * 60 * 1000; // now minus 31 minutes + const previousSessionLastEventId = 99; + const previousSessionDeviceId = 'a7a96s8d'; + const previousSessionUserId = 'a7a96s8d'; + beforeEach(() => { + // setup expired previous session + setLegacyCookie( + apiKey, + previousSessionDeviceId, + previousSessionUserId, + undefined, + previousSessionId, + previousSessionLastEventTime, + previousSessionLastEventId, + ); + }); + + test('should send session events and replace with known user', () => { let payload: any = undefined; const send = jest.fn().mockImplementationOnce(async (_endpoint, p) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -898,6 +922,7 @@ describe('integration', () => { }; }); client.init(apiKey, 'user1@amplitude.com', { + deviceId: UUID(), defaultTracking: { sessions: true, }, @@ -920,77 +945,36 @@ describe('integration', () => { }, 1000); setTimeout(() => { - client.setUserId('user2@amplitude.com'); // effectively creates a new session - // Sends `session_end` event for previous user and session - // Sends `session_start` event for next user and session + client.setUserId('user2@amplitude.com'); }, 2000); return new Promise((resolve) => { setTimeout(() => { - // Tranform events to be grouped by session and ordered by time - // Helps assert that events will show up correctly on Amplitude UI - const compactEvents: { eventType: string; userId: string; sessionId: number; time: number }[] = - payload.events.map((e: BaseEvent) => ({ - eventType: e.event_type, - userId: e.user_id, - sessionId: e.session_id, - time: e.time, - })); - const compactEventsBySessionAndTime = Object.values( - compactEvents.reduce< - Record - >((acc, curr) => { - if (!acc[curr.sessionId]) { - acc[curr.sessionId] = []; - } - acc[curr.sessionId].push(curr); - return acc; - }, {}), - ) - .map((c) => c.sort((a, b) => a.time - b.time)) - .map((group) => group.map((c) => ({ eventType: c.eventType, userId: c.userId }))); - - // The order of events in the payload is sorted by time of track fn invokation - // and not consistent with the time property - // Session events have overwritten time property - // Assert that events grouped by session and ordered by time - expect(compactEventsBySessionAndTime).toEqual([ - [ - { - eventType: 'session_start', - userId: 'user1@amplitude.com', - }, - { - eventType: '$identify', - userId: 'user1@amplitude.com', - }, - { - eventType: 'Event in first session', - userId: 'user1@amplitude.com', - }, - { - eventType: 'session_end', - userId: 'user1@amplitude.com', - }, - ], - [ - { - eventType: 'session_start', - userId: 'user1@amplitude.com', - }, - { - eventType: 'Event in next session', - userId: 'user1@amplitude.com', - }, - ], - ]); expect(payload).toEqual({ api_key: apiKey, events: [ + { + // This is a `session_end` event for the previous session + device_id: previousSessionDeviceId, + event_id: 100, + event_type: 'session_end', + insert_id: uuid, + ip: '$remote', + language: 'en-US', + library, + os_name: 'WebKit', + os_version: '537.36', + partner_id: undefined, + plan: undefined, + platform: 'Web', + session_id: previousSessionId, + time: previousSessionId + 1, + user_agent: userAgent, + user_id: previousSessionUserId, + }, { device_id: uuid, - device_manufacturer: undefined, - event_id: 0, + event_id: 101, event_type: 'session_start', insert_id: uuid, ip: '$remote', @@ -1008,8 +992,7 @@ describe('integration', () => { }, { device_id: uuid, - device_manufacturer: undefined, - event_id: 1, + event_id: 102, event_type: '$identify', insert_id: uuid, ip: '$remote', @@ -1071,8 +1054,7 @@ describe('integration', () => { }, { device_id: uuid, - device_manufacturer: undefined, - event_id: 2, + event_id: 103, event_type: 'Event in first session', insert_id: uuid, ip: '$remote', @@ -1090,9 +1072,8 @@ describe('integration', () => { }, { device_id: uuid, - device_manufacturer: undefined, - event_id: 3, - event_type: 'Event in next session', + event_id: 104, + event_type: 'session_end', insert_id: uuid, ip: '$remote', language: 'en-US', @@ -1109,9 +1090,8 @@ describe('integration', () => { }, { device_id: uuid, - device_manufacturer: undefined, - event_id: 4, - event_type: 'session_end', + event_id: 105, + event_type: 'session_start', insert_id: uuid, ip: '$remote', language: 'en-US', @@ -1128,9 +1108,8 @@ describe('integration', () => { }, { device_id: uuid, - device_manufacturer: undefined, - event_id: 5, - event_type: 'session_start', + event_id: 106, + event_type: 'Event in next session', insert_id: uuid, ip: '$remote', language: 'en-US', @@ -1155,7 +1134,10 @@ describe('integration', () => { }); }); - test('should send session events and replaced with unknown user', () => { + test('should send session events and replace with unknown user', () => { + // Reset previous session cookies + document.cookie = `amp_${apiKey.substring(0, 6)}=null; expires=1 Jan 1970 00:00:00 GMT`; + let payload: any = undefined; const send = jest.fn().mockImplementationOnce(async (_endpoint, p) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -1193,87 +1175,21 @@ describe('integration', () => { }, 1000); setTimeout(() => { - client.reset(); // effectively creates a new session - // Sends `session_end` event for previous user and session - // Sends `session_start` event for next user and session + client.reset(); }, 2000); return new Promise((resolve) => { setTimeout(() => { - // Tranform events to be grouped by session and ordered by time - // Helps assert that events will show up correctly on Amplitude UI - const compactEvents: { - eventType: string; - userId: string; - sessionId: number; - time: number; - deviceId: string; - }[] = payload.events.map((e: BaseEvent) => ({ - eventType: e.event_type, - userId: e.user_id, - sessionId: e.session_id, - deviceId: e.device_id, - time: e.time, - })); - const compactEventsBySessionAndTime = Object.values( - compactEvents.reduce< - Record - >((acc, curr) => { - if (!acc[curr.sessionId]) { - acc[curr.sessionId] = []; - } - acc[curr.sessionId].push(curr); - return acc; - }, {}), - ) - .map((c) => c.sort((a, b) => a.time - b.time)) - .map((group) => group.map((c) => ({ eventType: c.eventType, userId: c.userId, deviceId: c.deviceId }))); - const deviceIds = compactEventsBySessionAndTime.flat().map((c) => c.deviceId); - expect(deviceIds[0]).toEqual(deviceIds[1]); - expect(deviceIds[1]).toEqual(deviceIds[2]); - expect(deviceIds[2]).toEqual(deviceIds[3]); - expect(deviceIds[3]).toEqual(deviceIds[4]); - expect(deviceIds[4]).toEqual(deviceIds[5]); - // The order of events in the payload is sorted by time of track fn invokation - // and not consistent with the time property - // Session events have overwritten time property - // Assert that events grouped by session and ordered by time - expect(compactEventsBySessionAndTime).toEqual([ - [ - { - eventType: 'session_start', - userId: 'user1@amplitude.com', - deviceId: uuid, - }, - { - eventType: '$identify', - userId: 'user1@amplitude.com', - deviceId: uuid, - }, - { - eventType: 'Event in first session', - userId: 'user1@amplitude.com', - deviceId: uuid, - }, - { - eventType: 'session_end', - userId: 'user1@amplitude.com', - deviceId: uuid, - }, - ], - [ - { - eventType: 'session_start', - userId: 'user1@amplitude.com', - deviceId: uuid, - }, - { - eventType: 'Event in next session', - userId: 'user1@amplitude.com', - deviceId: uuid, - }, - ], - ]); + const deviceId0 = payload.events[0].device_id; + [ + payload.events[1].device_id, + payload.events[2].device_id, + payload.events[3].device_id, + payload.events[4].device_id, + payload.events[5].device_id, + ].forEach((deviceId) => { + expect(deviceId).toEqual(deviceId0); + }); expect(payload).toEqual({ api_key: apiKey, events: [ @@ -1382,7 +1298,8 @@ describe('integration', () => { device_id: uuid, device_manufacturer: undefined, event_id: 3, - event_type: 'Event in next session', + event_type: 'session_end', + insert_id: uuid, ip: '$remote', language: 'en-US', @@ -1401,7 +1318,7 @@ describe('integration', () => { device_id: uuid, device_manufacturer: undefined, event_id: 4, - event_type: 'session_end', + event_type: 'session_start', insert_id: uuid, ip: '$remote', language: 'en-US', @@ -1420,7 +1337,7 @@ describe('integration', () => { device_id: uuid, device_manufacturer: undefined, event_id: 5, - event_type: 'session_start', + event_type: 'Event in next session', insert_id: uuid, ip: '$remote', language: 'en-US', @@ -1444,6 +1361,115 @@ describe('integration', () => { }, 4000); }); }); + + test('should handle expired session', async () => { + const scope = nock(url).post(path).reply(200, success); + await client.init(apiKey, undefined, { + ...opts, + defaultTracking, + }).promise; + const response = await client.track('test event', { + mode: 'test', + }).promise; + expect(response.event.session_id).not.toBe(previousSessionId); + expect(response.event).toEqual({ + user_id: previousSessionUserId, + device_id: previousSessionDeviceId, + session_id: number, + time: number, + platform: 'Web', + language: 'en-US', + ip: '$remote', + insert_id: uuid, + partner_id: undefined, + event_type: 'test event', + event_id: 100, + event_properties: { + mode: 'test', + }, + library: library, + os_name: 'WebKit', + os_version: '537.36', + user_agent: userAgent, + }); + expect(response.code).toBe(200); + expect(response.message).toBe(SUCCESS_MESSAGE); + expect(document.cookie.includes('amp_')).toBe(false); + scope.done(); + }); + + test('should handle expired session and set with options.sessionId', async () => { + const scope = nock(url).post(path).reply(200, success); + await client.init(apiKey, undefined, { + ...opts, + defaultTracking, + sessionId: 1, + }).promise; + const response = await client.track('test event', { + mode: 'test', + }).promise; + expect(response.event.session_id).toBe(1); + expect(response.event).toEqual({ + user_id: previousSessionUserId, + device_id: previousSessionDeviceId, + session_id: number, + time: number, + platform: 'Web', + language: 'en-US', + ip: '$remote', + insert_id: uuid, + partner_id: undefined, + event_type: 'test event', + event_id: 100, + event_properties: { + mode: 'test', + }, + library: library, + os_name: 'WebKit', + os_version: '537.36', + user_agent: userAgent, + }); + expect(response.code).toBe(200); + expect(response.message).toBe(SUCCESS_MESSAGE); + expect(document.cookie.includes('amp_')).toBe(false); + scope.done(); + }); + + test('should handle expired session and set with setSessionId', async () => { + const scope = nock(url).post(path).reply(200, success); + await client.init(apiKey, undefined, { + ...opts, + defaultTracking, + }).promise; + client.setSessionId(1); + const response = await client.track('test event', { + mode: 'test', + }).promise; + expect(response.event).toEqual({ + user_id: previousSessionUserId, + device_id: previousSessionDeviceId, + session_id: 1, + time: number, + platform: 'Web', + language: 'en-US', + ip: '$remote', + insert_id: uuid, + partner_id: undefined, + event_type: 'test event', + event_id: 100, + event_properties: { + mode: 'test', + }, + library: library, + os_name: 'WebKit', + os_version: '537.36', + user_agent: userAgent, + }); + expect(response.code).toBe(200); + expect(response.message).toBe(SUCCESS_MESSAGE); + expect(document.cookie.includes('amp_')).toBe(false); + scope.done(); + }); }); describe('default page view tracking', () => { @@ -1746,23 +1772,38 @@ describe('integration', () => { }); describe('cookie migration', () => { + const previousSessionId = Date.now(); // now minus 31 minutes + const previousSessionLastEventTime = Date.now(); // now minus 31 minutes + const previousSessionLastEventId = 99; + const previousSessionDeviceId = 'deviceId'; + const previousSessionUserId = 'userId'; + + beforeEach(() => { + // setup expired previous session + setLegacyCookie( + apiKey, + previousSessionDeviceId, + previousSessionUserId, + undefined, + previousSessionId, + previousSessionLastEventTime, + previousSessionLastEventId, + ); + }); + test('should use old cookies', async () => { const scope = nock(url).post(path).reply(200, success); - const timestamp = Date.now(); - const time = timestamp.toString(32); - const userId = 'userId'; - const encodedUserId = btoa(unescape(encodeURIComponent(userId))); - document.cookie = `amp_${apiKey.substring(0, 6)}=deviceId.${encodedUserId}..${time}.${time}`; await client.init(apiKey, undefined, { ...opts, + defaultTracking, }).promise; const response = await client.track('test event', { mode: 'test', }).promise; expect(response.event).toEqual({ - user_id: userId, - device_id: 'deviceId', - session_id: timestamp, + user_id: previousSessionUserId, + device_id: previousSessionDeviceId, + session_id: previousSessionId, time: number, platform: 'Web', os_name: 'WebKit', @@ -1773,7 +1814,7 @@ describe('integration', () => { insert_id: uuid, partner_id: undefined, event_type: 'test event', - event_id: 0, + event_id: 100, event_properties: { mode: 'test', }, @@ -1788,22 +1829,18 @@ describe('integration', () => { test('should retain old cookies', async () => { const scope = nock(url).post(path).reply(200, success); - const timestamp = Date.now(); - const time = timestamp.toString(32); - const userId = 'userId'; - const encodedUserId = btoa(unescape(encodeURIComponent(userId))); - document.cookie = `amp_${apiKey.substring(0, 6)}=deviceId.${encodedUserId}..${time}.${time}`; await client.init(apiKey, undefined, { ...opts, cookieUpgrade: false, + defaultTracking, }).promise; const response = await client.track('test event', { mode: 'test', }).promise; expect(response.event).toEqual({ - user_id: userId, - device_id: 'deviceId', - session_id: timestamp, + user_id: previousSessionUserId, + device_id: previousSessionDeviceId, + session_id: previousSessionId, time: number, platform: 'Web', os_name: 'WebKit', @@ -1814,7 +1851,7 @@ describe('integration', () => { insert_id: uuid, partner_id: undefined, event_type: 'test event', - event_id: 0, + event_id: 100, event_properties: { mode: 'test', }, @@ -1980,3 +2017,22 @@ describe('integration', () => { }); }); }); + +const setLegacyCookie = ( + apiKey: string, + deviceId?: string, + userId?: string, + optOut?: '1', + sessionId?: number, + lastEventTime?: number, + eventId?: number, +) => { + document.cookie = `amp_${apiKey.substring(0, 6)}=${[ + deviceId, + btoa(userId || ''), + optOut, + sessionId ? sessionId.toString(32) : '', + lastEventTime ? lastEventTime.toString(32) : '', + eventId ? eventId.toString(32) : '', + ].join('.')}`; +}; diff --git a/packages/analytics-browser/src/browser-client.ts b/packages/analytics-browser/src/browser-client.ts index 7027847cc..4215cd50d 100644 --- a/packages/analytics-browser/src/browser-client.ts +++ b/packages/analytics-browser/src/browser-client.ts @@ -15,6 +15,7 @@ import { BrowserClient, BrowserConfig, BrowserOptions, + Event, EventOptions, Identify as IIdentify, Revenue as IRevenue, @@ -27,7 +28,6 @@ import { useBrowserConfig, createTransport, getTopLevelDomain, createCookieStora import { parseLegacyCookies } from './cookie-migration'; import { webAttributionPlugin } from '@amplitude/plugin-web-attribution-browser'; import { pageViewTrackingPlugin } from '@amplitude/plugin-page-view-tracking-browser'; -import { sessionHandlerPlugin } from './plugins/session-handler'; import { formInteractionTracking } from './plugins/form-interaction-tracking'; import { fileDownloadTracking } from './plugins/file-download-tracking'; import { DEFAULT_PAGE_VIEW_EVENT, DEFAULT_SESSION_END_EVENT, DEFAULT_SESSION_START_EVENT } from './constants'; @@ -59,9 +59,9 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { // Step 2: Create browser config const deviceId = options.deviceId ?? queryParams.deviceId ?? previousCookies?.deviceId ?? legacyCookies.deviceId; - const sessionId = options.sessionId ?? previousCookies?.sessionId ?? legacyCookies.sessionId; + const sessionId = previousCookies?.sessionId ?? legacyCookies.sessionId; const optOut = options.optOut ?? previousCookies?.optOut ?? legacyCookies.optOut; - const lastEventId = previousCookies?.lastEventId; + const lastEventId = previousCookies?.lastEventId ?? legacyCookies.lastEventId; const lastEventTime = previousCookies?.lastEventTime ?? legacyCookies.lastEventTime; const userId = options.userId ?? previousCookies?.userId ?? legacyCookies.userId; @@ -81,16 +81,17 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { await super._init(browserOptions); - // Step 3: Manage session - let isNewSession = !this.config.lastEventTime; + // Step 3: Set session ID + let isNewSession = false; if ( + // user has never sent an event + !this.config.lastEventTime || + // user has no previous session ID !this.config.sessionId || + // has sent an event and has previous session but expired (this.config.lastEventTime && Date.now() - this.config.lastEventTime > this.config.sessionTimeout) ) { - // Either - // 1) No previous session; or - // 2) Previous session expired - this.setSessionId(Date.now()); + this.setSessionId(options.sessionId ?? this.config.sessionId ?? Date.now()); isNewSession = true; } @@ -107,7 +108,6 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { // Do not track any events before this await this.add(new Destination()).promise; await this.add(new Context()).promise; - await this.add(sessionHandlerPlugin()).promise; await this.add(new IdentityEventSender()).promise; if (isFileDownloadTrackingEnabled(this.config.defaultTracking)) { @@ -195,30 +195,40 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { this.q.push(this.setSessionId.bind(this, sessionId)); return; } + + // Prevents starting a new session with the same session ID + if (sessionId === this.config.sessionId) { + return; + } + const previousSessionId = this.getSessionId(); - const previousLastEventTime = this.config.lastEventTime; + const lastEventTime = this.config.lastEventTime; + let lastEventId = this.config.lastEventId ?? -1; this.config.sessionId = sessionId; this.config.lastEventTime = undefined; if (isSessionTrackingEnabled(this.config.defaultTracking)) { - if (previousSessionId && previousLastEventTime) { - const eventOptions: EventOptions = { + if (previousSessionId && lastEventTime) { + this.track(DEFAULT_SESSION_END_EVENT, undefined, { + device_id: this.previousSessionDeviceId, + event_id: ++lastEventId, session_id: previousSessionId, - time: previousLastEventTime + 1, - }; - eventOptions.device_id = this.previousSessionDeviceId; - eventOptions.user_id = this.previousSessionUserId; - this.track(DEFAULT_SESSION_END_EVENT, undefined, eventOptions); + time: lastEventTime + 1, + user_id: this.previousSessionUserId, + }); } + this.config.lastEventTime = this.config.sessionId; this.track(DEFAULT_SESSION_START_EVENT, undefined, { - session_id: sessionId, - time: sessionId - 1, + event_id: ++lastEventId, + session_id: this.config.sessionId, + time: this.config.lastEventTime, }); - this.previousSessionDeviceId = this.config.deviceId; - this.previousSessionUserId = this.config.userId; } + + this.previousSessionDeviceId = this.config.deviceId; + this.previousSessionUserId = this.config.userId; } extendSession() { @@ -269,4 +279,21 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { } return super.revenue(revenue, eventOptions); } + + async process(event: Event) { + const currentTime = Date.now(); + const lastEventTime = this.config.lastEventTime || Date.now(); + const timeSinceLastEvent = currentTime - lastEventTime; + + if ( + event.event_type !== DEFAULT_SESSION_START_EVENT && + event.event_type !== DEFAULT_SESSION_END_EVENT && + (!event.session_id || event.session_id === this.getSessionId()) && + timeSinceLastEvent > this.config.sessionTimeout + ) { + this.setSessionId(currentTime); + } + + return super.process(event); + } } diff --git a/packages/analytics-browser/src/cookie-migration/index.ts b/packages/analytics-browser/src/cookie-migration/index.ts index 751877e0e..81ebc22d1 100644 --- a/packages/analytics-browser/src/cookie-migration/index.ts +++ b/packages/analytics-browser/src/cookie-migration/index.ts @@ -2,7 +2,7 @@ import { BrowserOptions, UserSession } from '@amplitude/analytics-types'; import { getOldCookieName } from '@amplitude/analytics-client-common'; import { createCookieStorage, getDefaultConfig } from '../config'; -export const parseLegacyCookies = async (apiKey: string, options?: BrowserOptions): Promise => { +export const parseLegacyCookies = async (apiKey: string, options: BrowserOptions): Promise => { const storage = await createCookieStorage(options); const oldCookieName = getOldCookieName(apiKey); const cookies = await storage.getRaw(oldCookieName); @@ -13,14 +13,15 @@ export const parseLegacyCookies = async (apiKey: string, options?: BrowserOption }; } - if (options?.cookieUpgrade ?? getDefaultConfig().cookieUpgrade) { + if (options.cookieUpgrade ?? getDefaultConfig().cookieUpgrade) { await storage.remove(oldCookieName); } - const [deviceId, userId, optOut, sessionId, lastEventTime] = cookies.split('.'); + const [deviceId, userId, optOut, sessionId, lastEventTime, lastEventId] = cookies.split('.'); return { deviceId, userId: decode(userId), sessionId: parseTime(sessionId), + lastEventId: parseTime(lastEventId), lastEventTime: parseTime(lastEventTime), optOut: Boolean(optOut), }; diff --git a/packages/analytics-browser/src/plugins/context.ts b/packages/analytics-browser/src/plugins/context.ts index 63389648b..eef171c23 100644 --- a/packages/analytics-browser/src/plugins/context.ts +++ b/packages/analytics-browser/src/plugins/context.ts @@ -14,7 +14,6 @@ export class Context implements BeforePlugin { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore config: BrowserConfig; - eventId = 0; userAgent: string | undefined; uaResult: UAParser.IResult; library = `amplitude-ts/${VERSION}`; @@ -29,18 +28,24 @@ export class Context implements BeforePlugin { setup(config: BrowserConfig): Promise { this.config = config; - this.eventId = this.config.lastEventId ? this.config.lastEventId + 1 : 0; return Promise.resolve(undefined); } async execute(context: Event): Promise { const time = new Date().getTime(); + const osName = this.uaResult.browser.name; const osVersion = this.uaResult.browser.version; const deviceModel = this.uaResult.device.model || this.uaResult.os.name; const deviceVendor = this.uaResult.device.vendor; - this.config.lastEventId = this.eventId; + + const lastEventId = this.config.lastEventId ?? -1; + const nextEventId = context.event_id ?? lastEventId + 1; + this.config.lastEventId = nextEventId; + if (!context.time) { + this.config.lastEventTime = time; + } const event: Event = { user_id: this.config.userId, @@ -65,7 +70,7 @@ export class Context implements BeforePlugin { }, }), ...context, - event_id: this.eventId++, + event_id: nextEventId, library: this.library, user_agent: this.userAgent, }; diff --git a/packages/analytics-browser/src/plugins/session-handler.ts b/packages/analytics-browser/src/plugins/session-handler.ts deleted file mode 100644 index edde0ede7..000000000 --- a/packages/analytics-browser/src/plugins/session-handler.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { BeforePlugin, BrowserClient, BrowserConfig, Event, PluginType } from '@amplitude/analytics-types'; -import { DEFAULT_SESSION_END_EVENT, DEFAULT_SESSION_START_EVENT } from '../constants'; - -export const sessionHandlerPlugin = (): BeforePlugin => { - // browserConfig is defined in setup() which will always be called first - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - let browserConfig: BrowserConfig; - // amplitude is defined in setup() which will always be called first - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - let amplitude: BrowserClient; - - const name = '@amplitude/plugin-session-handler'; - - const type = PluginType.BEFORE; - - const setup = async (config: BrowserConfig, client: BrowserClient) => { - browserConfig = config; - amplitude = client; - }; - - const execute = async (event: Event) => { - const now = Date.now(); - - if (event.event_type === DEFAULT_SESSION_START_EVENT || event.event_type === DEFAULT_SESSION_END_EVENT) { - browserConfig.lastEventTime = now; - return event; - } - - const lastEventTime = browserConfig.lastEventTime || now; - const timeSinceLastEvent = now - lastEventTime; - - if (timeSinceLastEvent > browserConfig.sessionTimeout) { - // assigns new session - amplitude.setSessionId(now); - event.session_id = amplitude.getSessionId(); - event.time = now; - } // else use existing session - - // updates last event time to extend time-based session - browserConfig.lastEventTime = now; - - return event; - }; - - return { - name, - type, - setup, - execute, - }; -}; diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index 78615b89d..fce55fcf0 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -3,7 +3,7 @@ import * as core from '@amplitude/analytics-core'; import * as Config from '../src/config'; import * as CookieMigration from '../src/cookie-migration'; import { Status, TransportType, UserSession } from '@amplitude/analytics-types'; -import { FetchTransport, getAnalyticsConnector } from '@amplitude/analytics-client-common'; +import { FetchTransport, getAnalyticsConnector, getCookieName } from '@amplitude/analytics-client-common'; import * as SnippetHelper from '../src/utils/snippet-helper'; import * as fileDownloadTracking from '../src/plugins/file-download-tracking'; import * as formInteractionTracking from '../src/plugins/form-interaction-tracking'; @@ -19,6 +19,13 @@ describe('browser-client', () => { disabled: true, }, }; + let apiKey = ''; + let client = new AmplitudeBrowser(); + + beforeEach(() => { + apiKey = core.UUID(); + client = new AmplitudeBrowser(); + }); afterEach(() => { // clean up cookies @@ -43,9 +50,15 @@ describe('browser-client', () => { optOut: false, lastEventTime: Date.now(), }); + const cookieStorage = new core.MemoryStorage(); + await cookieStorage.set(getCookieName(API_KEY), { + optOut: false, + lastEventId: 100, + }); const client = new AmplitudeBrowser(); await client.init(API_KEY, USER_ID, { sessionId: Date.now(), + cookieStorage, }).promise; expect(parseLegacyCookies).toHaveBeenCalledTimes(1); }); @@ -68,6 +81,7 @@ describe('browser-client', () => { deviceId: DEVICE_ID, sessionId: 1, lastEventTime: Date.now() - 1000, + lastEventId: 100, }); const cookieStorage = new core.MemoryStorage(); const client = new AmplitudeBrowser(); @@ -220,13 +234,12 @@ describe('browser-client', () => { jest.spyOn(CookieMigration, 'parseLegacyCookies').mockResolvedValueOnce({ optOut: false, lastEventTime: Date.now(), + sessionId: Date.now(), }); const client = new AmplitudeBrowser(); const webAttributionPluginPlugin = jest.spyOn(webAttributionPlugin, 'webAttributionPlugin'); await client.init(API_KEY, USER_ID, { optOut: false, - attribution: {}, - sessionId: Date.now(), transportProvider: { send: async () => ({ status: Status.Success, @@ -246,6 +259,7 @@ describe('browser-client', () => { jest.spyOn(CookieMigration, 'parseLegacyCookies').mockResolvedValueOnce({ optOut: false, lastEventTime: Date.now(), + sessionId: Date.now(), }); const client = new AmplitudeBrowser(); const webAttributionPluginPlugin = jest.spyOn(webAttributionPlugin, 'webAttributionPlugin'); @@ -254,7 +268,6 @@ describe('browser-client', () => { attribution: { trackNewCampaigns: true, }, - sessionId: Date.now(), transportProvider: { send: async () => ({ status: Status.Success, @@ -503,13 +516,14 @@ describe('browser-client', () => { }).promise; client.setSessionId(2); expect(client.getSessionId()).toBe(2); - expect(track).toHaveBeenCalledTimes(1); + expect(track).toHaveBeenCalledTimes(3); }); test('should set session id with start and end session event', async () => { jest.spyOn(CookieMigration, 'parseLegacyCookies').mockResolvedValueOnce({ optOut: false, sessionId: 1, + lastEventId: 100, lastEventTime: Date.now() - 1000, }); const client = new AmplitudeBrowser(); @@ -556,6 +570,7 @@ describe('browser-client', () => { const firstSessionId = 1; const client = new AmplitudeBrowser(); await client.init(API_KEY, undefined, { + ...attributionConfig, sessionTimeout: 20, sessionId: firstSessionId, flushQueueSize: 1, @@ -834,4 +849,52 @@ describe('browser-client', () => { expect(convertProxyObjectToRealObject).toHaveBeenCalledTimes(1); }); }); + + describe('process', () => { + test('should proceed with unexpired session', async () => { + const setSessionId = jest.spyOn(client, 'setSessionId'); + await client.init(apiKey, undefined, { + optOut: true, + ...attributionConfig, + }).promise; + const result = await client.process({ + event_type: 'event', + }); + // once on init + expect(setSessionId).toHaveBeenCalledTimes(1); + expect(result.code).toBe(0); + }); + + test('should proceed with overriden session ID', async () => { + const setSessionId = jest.spyOn(client, 'setSessionId'); + await client.init(apiKey, undefined, { + optOut: true, + ...attributionConfig, + }).promise; + const result = await client.process({ + event_type: 'event', + session_id: -1, + }); + // once on init + expect(setSessionId).toHaveBeenCalledTimes(1); + expect(result.code).toBe(0); + }); + + test('should reset session due to expired session', async () => { + const setSessionId = jest.spyOn(client, 'setSessionId'); + await client.init(apiKey, undefined, { + optOut: true, + ...attributionConfig, + // force session to always be expired + sessionTimeout: -1, + }).promise; + const result = await client.process({ + event_type: 'event', + }); + // once on init + // and once on process + expect(setSessionId).toHaveBeenCalledTimes(2); + expect(result.code).toBe(0); + }); + }); }); diff --git a/packages/analytics-browser/test/config.test.ts b/packages/analytics-browser/test/config.test.ts index 133a17e07..ab583cbf2 100644 --- a/packages/analytics-browser/test/config.test.ts +++ b/packages/analytics-browser/test/config.test.ts @@ -114,6 +114,7 @@ describe('config', () => { deviceId: 'device-device-device', sessionId: -1, userId: 'user-user-user', + lastEventId: 100, lastEventTime: 1, optOut: false, }); @@ -124,6 +125,7 @@ describe('config', () => { deviceId: 'device-device-device', sessionId: -1, userId: 'user-user-user', + lastEventId: 100, lastEventTime: 1, partnerId: 'partnerId', plan: { @@ -151,6 +153,7 @@ describe('config', () => { flushIntervalMillis: 1000, flushMaxRetries: 5, flushQueueSize: 30, + _lastEventId: 100, _lastEventTime: 1, loggerProvider: logger, logLevel: LogLevel.Warn, diff --git a/packages/analytics-browser/test/cookie-migration/index.test.ts b/packages/analytics-browser/test/cookie-migration/index.test.ts index 70939a69d..941210309 100644 --- a/packages/analytics-browser/test/cookie-migration/index.test.ts +++ b/packages/analytics-browser/test/cookie-migration/index.test.ts @@ -1,5 +1,5 @@ import { CookieStorage, getOldCookieName } from '@amplitude/analytics-client-common'; -import { Storage } from '@amplitude/analytics-types'; +import { Storage, UserSession } from '@amplitude/analytics-types'; import { decode, parseLegacyCookies, parseTime } from '../../src/cookie-migration'; import * as LocalStorageModule from '../../src/storage/local-storage'; @@ -39,12 +39,15 @@ describe('cookie-migration', () => { const userId = 'userId'; const encodedUserId = btoa(unescape(encodeURIComponent(userId))); const oldCookieName = getOldCookieName(API_KEY); - document.cookie = `${oldCookieName}=deviceId.${encodedUserId}..${time}.${time}`; - const cookies = await parseLegacyCookies(API_KEY); + const lastEventId = (0).toString(32); + document.cookie = `${oldCookieName}=deviceId.${encodedUserId}..${time}.${time}.${lastEventId}`; + const cookieStorage: Storage = new CookieStorage(); + const cookies = await parseLegacyCookies(API_KEY, { cookieStorage }); expect(cookies).toEqual({ deviceId: 'deviceId', userId: 'userId', sessionId: timestamp, + lastEventId: 0, lastEventTime: timestamp, optOut: false, }); @@ -60,15 +63,16 @@ describe('cookie-migration', () => { const userId = 'userId'; const encodedUserId = btoa(unescape(encodeURIComponent(userId))); const oldCookieName = getOldCookieName(API_KEY); - document.cookie = `${oldCookieName}=deviceId.${encodedUserId}..${time}.${time}`; - const cookies = await parseLegacyCookies(API_KEY, { - cookieUpgrade: true, - }); + const lastEventId = (0).toString(32); + document.cookie = `${oldCookieName}=deviceId.${encodedUserId}..${time}.${time}.${lastEventId}`; + const cookieStorage: Storage = new CookieStorage(); + const cookies = await parseLegacyCookies(API_KEY, { cookieStorage, cookieUpgrade: true }); expect(cookies).toEqual({ deviceId: 'deviceId', userId: 'userId', sessionId: timestamp, lastEventTime: timestamp, + lastEventId: 0, optOut: false, }); @@ -83,21 +87,22 @@ describe('cookie-migration', () => { const userId = 'userId'; const encodedUserId = btoa(unescape(encodeURIComponent(userId))); const oldCookieName = getOldCookieName(API_KEY); - document.cookie = `${oldCookieName}=deviceId.${encodedUserId}..${time}.${time}`; - const cookies = await parseLegacyCookies(API_KEY, { - cookieUpgrade: false, - }); + const lastEventId = (0).toString(32); + document.cookie = `${oldCookieName}=deviceId.${encodedUserId}..${time}.${time}.${lastEventId}`; + const cookieStorage: Storage = new CookieStorage(); + const cookies = await parseLegacyCookies(API_KEY, { cookieStorage, cookieUpgrade: false }); expect(cookies).toEqual({ deviceId: 'deviceId', userId: 'userId', sessionId: timestamp, lastEventTime: timestamp, + lastEventId: 0, optOut: false, }); const storage: Storage = new CookieStorage(); const cookies2 = await storage.getRaw(oldCookieName); - expect(cookies2).toBe(`deviceId.${encodedUserId}..${time}.${time}`); + expect(cookies2).toBe(`deviceId.${encodedUserId}..${time}.${time}.${lastEventId}`); }); }); diff --git a/packages/analytics-browser/test/plugins/context.test.ts b/packages/analytics-browser/test/plugins/context.test.ts index 4c30acd8e..0e622fde2 100644 --- a/packages/analytics-browser/test/plugins/context.test.ts +++ b/packages/analytics-browser/test/plugins/context.test.ts @@ -10,7 +10,6 @@ describe('context', () => { config.lastEventId = 1; await context.setup(config); expect(context.config.appVersion).toEqual('1.0.0'); - expect(context.eventId).toEqual(2); expect(context.uaResult).toBeDefined(); }); @@ -19,7 +18,6 @@ describe('context', () => { const config = useDefaultConfig(); await context.setup(config); expect(context.config.appVersion).toBeUndefined(); - expect(context.eventId).toEqual(0); expect(context.uaResult).toBeDefined(); }); }); @@ -112,15 +110,18 @@ describe('context', () => { event_type: 'event_type', device_id: 'new deviceId', }; - const firstContextEvent = await context.execute(event); + const firstContextEvent = await context.execute({ + ...event, + event_id: 100, + }); expect(firstContextEvent.app_version).toEqual('1.0.0'); - expect(firstContextEvent.event_id).toEqual(0); + expect(firstContextEvent.event_id).toEqual(100); expect(firstContextEvent.event_type).toEqual('event_type'); expect(firstContextEvent.insert_id).toBeDefined(); expect(firstContextEvent.device_id).toEqual('new deviceId'); const secondContextEvent = await context.execute(event); - expect(secondContextEvent.event_id).toEqual(1); + expect(secondContextEvent.event_id).toEqual(101); }); describe('ingestionMetadata config', () => { diff --git a/packages/analytics-browser/test/plugins/session-handler.test.ts b/packages/analytics-browser/test/plugins/session-handler.test.ts deleted file mode 100644 index 413ad054d..000000000 --- a/packages/analytics-browser/test/plugins/session-handler.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* eslint-disable @typescript-eslint/unbound-method */ - -import { createAmplitudeMock, createConfigurationMock } from '../helpers/mock'; -import { sessionHandlerPlugin } from '../../src/plugins/session-handler'; - -describe('sessionHandlerPlugin', () => { - let amplitude = createAmplitudeMock(); - - beforeEach(() => { - amplitude = createAmplitudeMock(); - }); - - test('should use the existing session', async () => { - const plugin = sessionHandlerPlugin(); - const time = Date.now(); - const config = createConfigurationMock({ - sessionId: 1, - lastEventTime: time - 999, - sessionTimeout: 1000, - }); - await plugin.setup(config, amplitude); - await plugin.execute({ - event_type: 'Test Event', - }); - - // assert session id is unchanged - expect(config.sessionId).toBe(1); - - // assert last event time was updated - expect(config.lastEventTime).not.toBe(time - 999); - - // assert no events are instrumented - expect(amplitude.track).toHaveBeenCalledTimes(0); - }); - - test('should use the existing session with no lastEventTime', async () => { - const plugin = sessionHandlerPlugin(); - const config = createConfigurationMock({ - sessionId: 1, - lastEventTime: undefined, - sessionTimeout: 1000, - }); - await plugin.setup(config, amplitude); - await plugin.execute({ - event_type: 'Test Event', - }); - - // assert session id is unchanged - expect(config.sessionId).toBe(1); - - // assert last event time was updated - expect(config.lastEventTime).not.toBeUndefined(); - - // assert no events are instrumented - expect(amplitude.track).toHaveBeenCalledTimes(0); - }); - - test('should assign new session', async () => { - const plugin = sessionHandlerPlugin(); - const time = Date.now(); - const config = createConfigurationMock({ - sessionId: 1, - lastEventTime: time - 1001, - sessionTimeout: 1000, - defaultTracking: { - sessions: true, - }, - }); - await plugin.setup(config, amplitude); - await plugin.execute({ - event_type: 'Test Event', - }); - - // assert session id was changed - expect(amplitude.setSessionId).toHaveBeenCalledTimes(1); - }); - - test.each(['session_start', 'session_end'])('should handle session events', async (eventType) => { - const plugin = sessionHandlerPlugin(); - const time = Date.now(); - const config = createConfigurationMock({ - sessionId: 1, - lastEventTime: time - 1001, - sessionTimeout: 1000, - defaultTracking: { - sessions: true, - }, - }); - await plugin.setup(config, amplitude); - await plugin.execute({ - event_type: eventType, - }); - - // assert session id was changed - expect(amplitude.setSessionId).toHaveBeenCalledTimes(0); - }); -}); From 018337284d2bab9d35414e4a14f720b1234ff8ab Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Wed, 14 Jun 2023 03:35:12 +0000 Subject: [PATCH 016/214] chore(release): publish - @amplitude/analytics-browser-test@1.4.4 - @amplitude/analytics-browser@1.11.1 - @amplitude/marketing-analytics-browser@1.0.4 - @amplitude/plugin-page-view-tracking-browser@1.0.4 - @amplitude/plugin-web-attribution-browser@1.0.4 --- packages/analytics-browser-test/CHANGELOG.md | 14 ++++++++++++++ packages/analytics-browser-test/package.json | 4 ++-- packages/analytics-browser/CHANGELOG.md | 14 ++++++++++++++ packages/analytics-browser/README.md | 2 +- .../generated/amplitude-snippet.js | 4 ++-- packages/analytics-browser/package.json | 6 +++--- packages/analytics-browser/playground/amplitude.js | 2 +- packages/analytics-browser/src/version.ts | 2 +- packages/marketing-analytics-browser/CHANGELOG.md | 9 +++++++++ packages/marketing-analytics-browser/README.md | 2 +- .../generated/amplitude-gtm-snippet.js | 4 ++-- .../generated/amplitude-snippet.js | 4 ++-- packages/marketing-analytics-browser/package.json | 8 ++++---- .../marketing-analytics-browser/src/version.ts | 2 +- .../plugin-page-view-tracking-browser/CHANGELOG.md | 9 +++++++++ .../plugin-page-view-tracking-browser/package.json | 4 ++-- .../plugin-web-attribution-browser/CHANGELOG.md | 9 +++++++++ .../plugin-web-attribution-browser/package.json | 4 ++-- 18 files changed, 79 insertions(+), 24 deletions(-) diff --git a/packages/analytics-browser-test/CHANGELOG.md b/packages/analytics-browser-test/CHANGELOG.md index ff4138537..ce25fbe9e 100644 --- a/packages/analytics-browser-test/CHANGELOG.md +++ b/packages/analytics-browser-test/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.4.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.3...@amplitude/analytics-browser-test@1.4.4) (2023-06-14) + +### Bug Fixes + +- session end events being assigned to a different session id + ([#426](https://github.com/amplitude/Amplitude-TypeScript/issues/426)) + ([#431](https://github.com/amplitude/Amplitude-TypeScript/issues/431)) + ([b46f2f4](https://github.com/amplitude/Amplitude-TypeScript/commit/b46f2f4ff9cce2848bdd9bf554377ca7ed47983c)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.4.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.2...@amplitude/analytics-browser-test@1.4.3) (2023-06-13) **Note:** Version bump only for package @amplitude/analytics-browser-test diff --git a/packages/analytics-browser-test/package.json b/packages/analytics-browser-test/package.json index 7761cb358..413c6b862 100644 --- a/packages/analytics-browser-test/package.json +++ b/packages/analytics-browser-test/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser-test", - "version": "1.4.3", + "version": "1.4.4", "private": true, "description": "", "author": "Amplitude Inc", @@ -16,7 +16,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.11.0" + "@amplitude/analytics-browser": "^1.11.1" }, "devDependencies": { "nock": "^13.2.4" diff --git a/packages/analytics-browser/CHANGELOG.md b/packages/analytics-browser/CHANGELOG.md index bbb729bb4..554ee24b8 100644 --- a/packages/analytics-browser/CHANGELOG.md +++ b/packages/analytics-browser/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.11.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.11.0...@amplitude/analytics-browser@1.11.1) (2023-06-14) + +### Bug Fixes + +- session end events being assigned to a different session id + ([#426](https://github.com/amplitude/Amplitude-TypeScript/issues/426)) + ([#431](https://github.com/amplitude/Amplitude-TypeScript/issues/431)) + ([b46f2f4](https://github.com/amplitude/Amplitude-TypeScript/commit/b46f2f4ff9cce2848bdd9bf554377ca7ed47983c)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [1.11.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.10.8...@amplitude/analytics-browser@1.11.0) (2023-06-13) ### Features diff --git a/packages/analytics-browser/README.md b/packages/analytics-browser/README.md index b39f0da86..c248d306c 100644 --- a/packages/analytics-browser/README.md +++ b/packages/analytics-browser/README.md @@ -40,7 +40,7 @@ Alternatively, the package is also distributed through a CDN. Copy and paste the ```html diff --git a/packages/analytics-browser/generated/amplitude-snippet.js b/packages/analytics-browser/generated/amplitude-snippet.js index ee6a95c3b..ac7bb52b9 100644 --- a/packages/analytics-browser/generated/amplitude-snippet.js +++ b/packages/analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-88G7ldPwlcBAEMvub2fJXyrErbB+r+yi8fkKzYrr3iUDAkNi/0Y6kR2jyCMC1rfJ'; + as.integrity = 'sha384-So8wDvkfueWi1vtYoaT+2UMVdV5gytnrAUr8sumYyMQo4TNK4Lpct6VewTWsSNoH'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.11.0-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.11.1-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/analytics-browser/package.json b/packages/analytics-browser/package.json index 73ae2af67..462ed8a8c 100644 --- a/packages/analytics-browser/package.json +++ b/packages/analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser", - "version": "1.11.0", + "version": "1.11.1", "description": "Official Amplitude SDK for Web", "keywords": [ "analytics", @@ -46,8 +46,8 @@ "@amplitude/analytics-client-common": "^1.0.2", "@amplitude/analytics-core": "^1.1.1", "@amplitude/analytics-types": "^1.2.0", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.3", - "@amplitude/plugin-web-attribution-browser": "^1.0.3", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.4", + "@amplitude/plugin-web-attribution-browser": "^1.0.4", "@amplitude/ua-parser-js": "^0.7.31", "tslib": "^2.4.1" }, diff --git a/packages/analytics-browser/playground/amplitude.js b/packages/analytics-browser/playground/amplitude.js index 7f02b4256..057e50752 100644 --- a/packages/analytics-browser/playground/amplitude.js +++ b/packages/analytics-browser/playground/amplitude.js @@ -1 +1 @@ -!function(){"use strict";var e=function(t,i){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])},e(t,i)};function t(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=t}e(t,i),t.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var i=function(){return i=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!L(t,i))return!1}return!0},L=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&j(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return j(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},M=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||L(t,i)))},e}(),V=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},z=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},F=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return z(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),B="Event rejected due to exceeded retry count",$=function(e){return{promise:e||Promise.resolve()}},Q=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new F(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return $(this.dispatch(r))},e.prototype.identify=function(e,t){var i=V(e,t);return $(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return $(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new M;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return $(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return $(this.dispatch(n))},e.prototype.add=function(e){return this.config?$(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),$())},e.prototype.remove=function(e){return this.config?$(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),$())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(z(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,z(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=z(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return $(this.timeline.flush())},e}(),K=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return j(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),H="Amplitude Logger ",W=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=Y(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:return null===(s=o.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(s,e),[3,4]):("body"in s?this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(X(s))):this.fulfillRequest(e,s.statusCode,s.status),[2]);case 3:return a=o.sent(),u=(c=a)instanceof Error?c.message:String(c),this.config.loggerProvider.error(u),this.fulfillRequest(e,0,u),[3,4];case 4:return[2]}var c}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(X(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));d.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(z(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ne=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},re=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},oe=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},se=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=oe(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ae=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ue)},ce=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),le=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),de=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[A,t,e.substring(0,i)].filter(Boolean).join("_")},pe=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),fe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(le),ve="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},he={exports:{}};ee=he,te=he.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",q="Xiaomi",A="Zebra",D="Facebook",C=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},z=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?M(e,275):e,this},this.setUA(f),this};$.VERSION="0.7.31",$.BROWSER=C([a,l,"major"]),$.CPU=C([d]),$.DEVICE=C([s,c,u,p,f,h,v,g,b]),$.ENGINE=$.OS=C([a,l]),ee.exports&&(te=ee.exports=$),te.UAParser=$;var Q=typeof e!==n&&(e.jQuery||e.Zepto);if(Q&&!Q.ua){var K=new $;Q.ua=K.getResult(),Q.ua.get=function(){return K.getUA()},Q.ua.set=function(e){K.setUA(e);var t=K.getResult();for(var i in t)Q.ua[i]=t[i]}}}("object"==typeof window?window:ve);var ge=he.exports,be=function(){function e(){this.ua=new he.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:we(),platform:"Web",os:ye(this.ua),deviceModel:me(this.ua)}},e}(),ye=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},me=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},we=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},_e=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),Ie=function(){return Ie=Object.assign||function(e){for(var t,i=1,n=arguments.length;ie.sessionTimeout&&(t.setSessionId(n),i.session_id=t.getSessionId(),i.time=n),e.lastEventTime=n,[2,i])}))}))}}},yt={page_domain:"".concat(it," Page Domain"),page_location:"".concat(it," Page Location"),page_path:"".concat(it," Page Path"),page_title:"".concat(it," Page Title"),page_url:"".concat(it," Page URL")},mt=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.init=function(e,t,n){return void 0===e&&(e=""),$(this._init(i(i({},n),{userId:t,apiKey:e})))},n.prototype._init=function(t){var n,s,u,c,l,d,p,v,h,g,b,m,w,_,I,k,E,S;return r(this,void 0,void 0,(function(){var T,O,x,P,R,N,q,A,D,C,j,L,M,V,z,F,B,$,Q=this;return o(this,(function(K){switch(K.label){case 0:return this.initializing?[2]:(this.initializing=!0,T=t,t.disableCookies?(O="",[3,5]):[3,1]);case 1:return null===(n=t.domain)||void 0===n?[3,2]:(x=n,[3,4]);case 2:return[4,r(void 0,void 0,void 0,(function(){var e,t,i,n,r,s,a;return o(this,(function(o){switch(o.label){case 0:return[4,(new pe).isEnabled()];case 1:if(!o.sent()||!W&&"undefined"==typeof location)return[2,""];for(e=null!=W?W:location.hostname,t=e.split("."),i=[],n="AMP_TLDTEST",r=t.length-2;r>=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(Date.now()),z=!0),(F=Oe()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new ie).promise];case 11:return K.sent(),[4,this.add(new qe).promise];case 12:return K.sent(),[4,this.add(bt()).promise];case 13:return K.sent(),[4,this.add(new xe).promise];case 14:return K.sent(),("boolean"==typeof(H=this.config.defaultTracking)?H:null==H?void 0:H.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(st,((n={})[ct]=o,n[lt]=i.pathname,n[dt]=e.id,n[pt]=e.text,n[ft]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,16];case 15:K.sent(),K.label=16;case 16:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),t.track(ot,((r={})[vt]=e.id,r[ht]=e.name,r[gt]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,18];case 17:K.sent(),K.label=18;case 18:return(null===(_=this.config.attribution)||void 0===_?void 0:_.disabled)?[3,20]:(B=function(){for(var e,t,n=this,s=[],u=0;u0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!L(t,i))return!1}return!0},L=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&j(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return j(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},M=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||L(t,i)))},e}(),V=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},z=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},F=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return z(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),B="Event rejected due to exceeded retry count",$=function(e){return{promise:e||Promise.resolve()}},Q=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new F(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return $(this.dispatch(r))},e.prototype.identify=function(e,t){var i=V(e,t);return $(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return $(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new M;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return $(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return $(this.dispatch(n))},e.prototype.add=function(e){return this.config?$(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),$())},e.prototype.remove=function(e){return this.config?$(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),$())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(z(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,z(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=z(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return $(this.timeline.flush())},e}(),K=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return j(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),H="Amplitude Logger ",W=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=Y(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:return null===(s=o.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(s,e),[3,4]):("body"in s?this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(X(s))):this.fulfillRequest(e,s.statusCode,s.status),[2]);case 3:return a=o.sent(),u=(c=a)instanceof Error?c.message:String(c),this.config.loggerProvider.error(u),this.fulfillRequest(e,0,u),[3,4];case 4:return[2]}var c}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(X(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));d.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(z(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ne=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},re=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},oe=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},se=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=oe(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ae=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ue)},ce=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),le=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),de=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[D,t,e.substring(0,i)].filter(Boolean).join("_")},pe=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),fe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(le),ve="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},he={exports:{}};ee=he,te=he.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",q="Xiaomi",D="Zebra",A="Facebook",C=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},z=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?M(e,275):e,this},this.setUA(f),this};$.VERSION="0.7.31",$.BROWSER=C([a,l,"major"]),$.CPU=C([d]),$.DEVICE=C([s,c,u,p,f,h,v,g,b]),$.ENGINE=$.OS=C([a,l]),ee.exports&&(te=ee.exports=$),te.UAParser=$;var Q=typeof e!==n&&(e.jQuery||e.Zepto);if(Q&&!Q.ua){var K=new $;Q.ua=K.getResult(),Q.ua.get=function(){return K.getUA()},Q.ua.set=function(e){K.setUA(e);var t=K.getResult();for(var i in t)Q.ua[i]=t[i]}}}("object"==typeof window?window:ve);var ge=he.exports,be=function(){function e(){this.ua=new he.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:we(),platform:"Web",os:ye(this.ua),deviceModel:me(this.ua)}},e}(),ye=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},me=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},we=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},_e=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),Ie=function(){return Ie=Object.assign||function(e){for(var t,i=1,n=arguments.length;i=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(null!==(I=null!==(_=t.sessionId)&&void 0!==_?_:this.config.sessionId)&&void 0!==I?I:Date.now()),B=!0),($=Oe()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new ie).promise];case 11:return W.sent(),[4,this.add(new qe).promise];case 12:return W.sent(),[4,this.add(new xe).promise];case 13:return W.sent(),("boolean"==typeof(Z=this.config.defaultTracking)?Z:null==Z?void 0:Z.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(st,((n={})[ct]=o,n[lt]=i.pathname,n[dt]=e.id,n[pt]=e.text,n[ft]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,15];case 14:W.sent(),W.label=15;case 15:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),t.track(ot,((r={})[vt]=e.id,r[ht]=e.name,r[gt]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,17];case 16:W.sent(),W.label=17;case 17:return(null===(k=this.config.attribution)||void 0===k?void 0:k.disabled)?[3,19]:(Q=function(){for(var e,t,n=this,s=[],u=0;uthis.config.sessionTimeout&&this.setSessionId(i),[2,e.prototype.process.call(this,t)]}))}))},n}(Q),mt=function(){var e=new yt;return{init:ae(e.init.bind(e),"init",re(e),se(e,["config"])),add:ae(e.add.bind(e),"add",re(e),se(e,["config.apiKey","timeline.plugins"])),remove:ae(e.remove.bind(e),"remove",re(e),se(e,["config.apiKey","timeline.plugins"])),track:ae(e.track.bind(e),"track",re(e),se(e,["config.apiKey","timeline.queue.length"])),logEvent:ae(e.logEvent.bind(e),"logEvent",re(e),se(e,["config.apiKey","timeline.queue.length"])),identify:ae(e.identify.bind(e),"identify",re(e),se(e,["config.apiKey","timeline.queue.length"])),groupIdentify:ae(e.groupIdentify.bind(e),"groupIdentify",re(e),se(e,["config.apiKey","timeline.queue.length"])),setGroup:ae(e.setGroup.bind(e),"setGroup",re(e),se(e,["config.apiKey","timeline.queue.length"])),revenue:ae(e.revenue.bind(e),"revenue",re(e),se(e,["config.apiKey","timeline.queue.length"])),flush:ae(e.flush.bind(e),"flush",re(e),se(e,["config.apiKey","timeline.queue.length"])),getUserId:ae(e.getUserId.bind(e),"getUserId",re(e),se(e,["config","config.userId"])),setUserId:ae(e.setUserId.bind(e),"setUserId",re(e),se(e,["config","config.userId"])),getDeviceId:ae(e.getDeviceId.bind(e),"getDeviceId",re(e),se(e,["config","config.deviceId"])),setDeviceId:ae(e.setDeviceId.bind(e),"setDeviceId",re(e),se(e,["config","config.deviceId"])),reset:ae(e.reset.bind(e),"reset",re(e),se(e,["config","config.userId","config.deviceId"])),getSessionId:ae(e.getSessionId.bind(e),"getSessionId",re(e),se(e,["config"])),setSessionId:ae(e.setSessionId.bind(e),"setSessionId",re(e),se(e,["config"])),extendSession:ae(e.extendSession.bind(e),"extendSession",re(e),se(e,["config"])),setOptOut:ae(e.setOptOut.bind(e),"setOptOut",re(e),se(e,["config"])),setTransport:ae(e.setTransport.bind(e),"setTransport",re(e),se(e,["config"]))}},wt=mt(),_t=wt.add,It=wt.extendSession,kt=wt.flush,Et=wt.getDeviceId,St=wt.getSessionId,Tt=wt.getUserId,Ot=wt.groupIdentify,xt=wt.identify,Pt=wt.init,Rt=wt.logEvent,Nt=wt.remove,Ut=wt.reset,qt=wt.revenue,Dt=wt.setDeviceId,At=wt.setGroup,Ct=wt.setOptOut,jt=wt.setSessionId,Lt=wt.setTransport,Mt=wt.setUserId,Vt=wt.track,zt=Object.freeze({__proto__:null,add:_t,extendSession:It,flush:kt,getDeviceId:Et,getSessionId:St,getUserId:Tt,groupIdentify:Ot,identify:xt,init:Pt,logEvent:Rt,remove:Nt,reset:Ut,revenue:qt,setDeviceId:Dt,setGroup:At,setOptOut:Ct,setSessionId:jt,setTransport:Lt,setUserId:Mt,track:Vt,Types:q,createInstance:mt,runQueuedFunctions:Re,Revenue:K,Identify:M});!function(){var e=b();if(e){var t=function(e){var t=mt(),i=b();return i&&i.amplitude&&i.amplitude._iq&&e&&(i.amplitude._iq[e]=t),t};if(e.amplitude=Object.assign(e.amplitude||{},zt,{createInstance:t}),e.amplitude.invoked){var i=e.amplitude._q;e.amplitude._q=[],Re(zt,i);for(var n=Object.keys(e.amplitude._iq)||[],r=0;r ```html diff --git a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js index f11bba734..c0a12184b 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-dqd1zhQwCaPNwnnxfckFnnSnDdhem8MpuxJOSB6Yvxc1uRXwkeyIsPXpUnbBIu4u'; + as.integrity = 'sha384-/dfCBvYaIDAyRKZDCpZNiEv+OiiACVEqHLTnwo/90WCzmEptJAIC5hOf2ebqCyBY'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.3-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.4-min.js.gz'; as.onload = function () { if (!window.amplitudeGTM.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/generated/amplitude-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-snippet.js index d416f439b..246f31480 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-Bnx/ydv/ChaWFeFAQkQJzhK20h7Daqih3HYpGGtC0BFpXuhOLf794kHvO2p7YuZW'; + as.integrity = 'sha384-PsQ24m4bUh9DzHyRSZbvWu1el7OYz39YDvB+JssWCe3O/9X0TmapEHWZBvi+HMqo'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.3-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.4-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/package.json b/packages/marketing-analytics-browser/package.json index b240dfe79..1639b3d6e 100644 --- a/packages/marketing-analytics-browser/package.json +++ b/packages/marketing-analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/marketing-analytics-browser", - "version": "1.0.3", + "version": "1.0.4", "description": "Official Amplitude SDK for Web and Marketing Analytics", "keywords": [ "analytics", @@ -42,12 +42,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.11.0", + "@amplitude/analytics-browser": "^1.11.1", "@amplitude/analytics-client-common": "^1.0.2", "@amplitude/analytics-core": "^1.1.1", "@amplitude/analytics-types": "^1.2.0", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.3", - "@amplitude/plugin-web-attribution-browser": "^1.0.3", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.4", + "@amplitude/plugin-web-attribution-browser": "^1.0.4", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/marketing-analytics-browser/src/version.ts b/packages/marketing-analytics-browser/src/version.ts index 30cbd1ac5..be9db6827 100644 --- a/packages/marketing-analytics-browser/src/version.ts +++ b/packages/marketing-analytics-browser/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.0.3'; +export const VERSION = '1.0.4'; diff --git a/packages/plugin-page-view-tracking-browser/CHANGELOG.md b/packages/plugin-page-view-tracking-browser/CHANGELOG.md index a7c738e6f..7118ce6ae 100644 --- a/packages/plugin-page-view-tracking-browser/CHANGELOG.md +++ b/packages/plugin-page-view-tracking-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.3...@amplitude/plugin-page-view-tracking-browser@1.0.4) (2023-06-14) + +**Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.2...@amplitude/plugin-page-view-tracking-browser@1.0.3) (2023-06-13) **Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser diff --git a/packages/plugin-page-view-tracking-browser/package.json b/packages/plugin-page-view-tracking-browser/package.json index 3c9ab818f..ed7548f7e 100644 --- a/packages/plugin-page-view-tracking-browser/package.json +++ b/packages/plugin-page-view-tracking-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-page-view-tracking-browser", - "version": "1.0.3", + "version": "1.0.4", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -41,7 +41,7 @@ "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.11.0", + "@amplitude/analytics-browser": "^1.11.1", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", diff --git a/packages/plugin-web-attribution-browser/CHANGELOG.md b/packages/plugin-web-attribution-browser/CHANGELOG.md index 046ebe285..bba49a28f 100644 --- a/packages/plugin-web-attribution-browser/CHANGELOG.md +++ b/packages/plugin-web-attribution-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.3...@amplitude/plugin-web-attribution-browser@1.0.4) (2023-06-14) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.2...@amplitude/plugin-web-attribution-browser@1.0.3) (2023-06-13) **Note:** Version bump only for package @amplitude/plugin-web-attribution-browser diff --git a/packages/plugin-web-attribution-browser/package.json b/packages/plugin-web-attribution-browser/package.json index a4e4358ea..81adaf24a 100644 --- a/packages/plugin-web-attribution-browser/package.json +++ b/packages/plugin-web-attribution-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-web-attribution-browser", - "version": "1.0.3", + "version": "1.0.4", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -41,7 +41,7 @@ "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.11.0", + "@amplitude/analytics-browser": "^1.11.1", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", From 9f0a180407168a1bb36779d56a816f99b181c20d Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Tue, 13 Jun 2023 21:03:15 -0700 Subject: [PATCH 017/214] build: update dist-tag (#418) --- packages/analytics-browser/package.json | 3 ++- packages/analytics-client-common/package.json | 3 ++- packages/analytics-core/package.json | 3 ++- packages/analytics-node/package.json | 3 ++- packages/analytics-react-native/package.json | 3 ++- packages/analytics-types/package.json | 3 ++- packages/marketing-analytics-browser/package.json | 3 ++- packages/plugin-page-view-tracking-browser/package.json | 3 ++- packages/plugin-web-attribution-browser/package.json | 3 ++- 9 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/analytics-browser/package.json b/packages/analytics-browser/package.json index 462ed8a8c..dcacb16a0 100644 --- a/packages/analytics-browser/package.json +++ b/packages/analytics-browser/package.json @@ -14,7 +14,8 @@ "types": "lib/esm/index.d.ts", "sideEffects": false, "publishConfig": { - "access": "public" + "access": "public", + "tag": "v1" }, "repository": { "type": "git", diff --git a/packages/analytics-client-common/package.json b/packages/analytics-client-common/package.json index 56f3cd90c..8d9385f15 100644 --- a/packages/analytics-client-common/package.json +++ b/packages/analytics-client-common/package.json @@ -10,7 +10,8 @@ "types": "lib/esm/index.d.ts", "sideEffects": false, "publishConfig": { - "access": "public" + "access": "public", + "tag": "v1" }, "repository": { "type": "git", diff --git a/packages/analytics-core/package.json b/packages/analytics-core/package.json index 273fdb53a..6e64e5dc2 100644 --- a/packages/analytics-core/package.json +++ b/packages/analytics-core/package.json @@ -10,7 +10,8 @@ "types": "lib/esm/index.d.ts", "sideEffects": false, "publishConfig": { - "access": "public" + "access": "public", + "tag": "v1" }, "repository": { "type": "git", diff --git a/packages/analytics-node/package.json b/packages/analytics-node/package.json index e5a9b5938..fe949f843 100644 --- a/packages/analytics-node/package.json +++ b/packages/analytics-node/package.json @@ -10,7 +10,8 @@ "types": "lib/esm/index.d.ts", "sideEffects": false, "publishConfig": { - "access": "public" + "access": "public", + "tag": "latest" }, "repository": { "type": "git", diff --git a/packages/analytics-react-native/package.json b/packages/analytics-react-native/package.json index 9076767de..99e00b055 100644 --- a/packages/analytics-react-native/package.json +++ b/packages/analytics-react-native/package.json @@ -17,7 +17,8 @@ "types": "lib/typescript/index.d.ts", "react-native": "src/index", "publishConfig": { - "access": "public" + "access": "public", + "tag": "latest" }, "repository": { "type": "git", diff --git a/packages/analytics-types/package.json b/packages/analytics-types/package.json index c8327869e..39405d163 100644 --- a/packages/analytics-types/package.json +++ b/packages/analytics-types/package.json @@ -10,7 +10,8 @@ "types": "lib/esm/index.d.ts", "sideEffects": false, "publishConfig": { - "access": "public" + "access": "public", + "tag": "v1" }, "repository": { "type": "git", diff --git a/packages/marketing-analytics-browser/package.json b/packages/marketing-analytics-browser/package.json index 1639b3d6e..4e9f68a3e 100644 --- a/packages/marketing-analytics-browser/package.json +++ b/packages/marketing-analytics-browser/package.json @@ -14,7 +14,8 @@ "types": "lib/esm/index.d.ts", "sideEffects": false, "publishConfig": { - "access": "public" + "access": "public", + "tag": "latest" }, "repository": { "type": "git", diff --git a/packages/plugin-page-view-tracking-browser/package.json b/packages/plugin-page-view-tracking-browser/package.json index ed7548f7e..245d00668 100644 --- a/packages/plugin-page-view-tracking-browser/package.json +++ b/packages/plugin-page-view-tracking-browser/package.json @@ -10,7 +10,8 @@ "types": "lib/esm/index.d.ts", "sideEffects": false, "publishConfig": { - "access": "public" + "access": "public", + "tag": "v1" }, "repository": { "type": "git", diff --git a/packages/plugin-web-attribution-browser/package.json b/packages/plugin-web-attribution-browser/package.json index 81adaf24a..f60c87a8f 100644 --- a/packages/plugin-web-attribution-browser/package.json +++ b/packages/plugin-web-attribution-browser/package.json @@ -10,7 +10,8 @@ "types": "lib/esm/index.d.ts", "sideEffects": false, "publishConfig": { - "access": "public" + "access": "public", + "tag": "v1" }, "repository": { "type": "git", From bab630dec042d01eb703bd92530e632052f2fe33 Mon Sep 17 00:00:00 2001 From: Justin Fiedler Date: Wed, 14 Jun 2023 08:12:45 -0700 Subject: [PATCH 018/214] fix: compact logged response body's for AMP-77261 (#430) --- packages/analytics-core/src/plugins/destination.ts | 2 +- packages/analytics-core/test/plugins/destination.test.ts | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index 602bf1c35..76d74a7fc 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -33,7 +33,7 @@ export function getResponseBodyString(res: Response) { let responseBodyString = ''; try { if ('body' in res) { - responseBodyString = JSON.stringify(res.body, null, 2); + responseBodyString = JSON.stringify(res.body); } } catch { // to avoid crash, but don't care about the error, add comment to avoid empty block lint error diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index 41345a513..8b8562d19 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -8,8 +8,6 @@ import { UNEXPECTED_ERROR_MESSAGE, } from '../../src/messages'; -const jsons = (obj: any) => JSON.stringify(obj, null, 2); - describe('destination', () => { describe('setup', () => { test('should setup plugin', async () => { @@ -375,7 +373,7 @@ describe('destination', () => { expect(callback).toHaveBeenCalledWith({ event, code: 400, - message: `${Status.Invalid}: ${jsons(body)}`, + message: `${Status.Invalid}: ${JSON.stringify(body)}`, }); }); @@ -989,7 +987,7 @@ describe('destination', () => { // eslint-disable-next-line @typescript-eslint/unbound-method expect(loggerProvider.warn).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/unbound-method,@typescript-eslint/restrict-template-expressions - expect(loggerProvider.warn).toHaveBeenCalledWith(jsons(response.body)); + expect(loggerProvider.warn).toHaveBeenCalledWith(JSON.stringify(response.body)); }, ); From 301ba1e94d67ead341bea225189d7794acdd6922 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Wed, 14 Jun 2023 17:15:55 +0000 Subject: [PATCH 019/214] chore(release): publish - @amplitude/analytics-browser-test@1.4.5 - @amplitude/analytics-browser@1.11.2 - @amplitude/analytics-client-common@1.0.3 - @amplitude/analytics-core@1.1.2 - @amplitude/analytics-node-test@1.0.3 - @amplitude/analytics-node@1.2.2 - @amplitude/analytics-react-native@1.2.2 - @amplitude/analytics-types@1.2.1 - @amplitude/marketing-analytics-browser@1.0.5 - @amplitude/plugin-page-view-tracking-browser@1.0.5 - @amplitude/plugin-web-attribution-browser@1.0.5 --- packages/analytics-browser-test/CHANGELOG.md | 9 +++++++++ packages/analytics-browser-test/package.json | 4 ++-- packages/analytics-browser/CHANGELOG.md | 9 +++++++++ packages/analytics-browser/README.md | 2 +- .../generated/amplitude-snippet.js | 4 ++-- packages/analytics-browser/package.json | 12 ++++++------ packages/analytics-browser/playground/amplitude.js | 2 +- packages/analytics-browser/src/version.ts | 2 +- packages/analytics-client-common/CHANGELOG.md | 9 +++++++++ packages/analytics-client-common/package.json | 6 +++--- packages/analytics-core/CHANGELOG.md | 12 ++++++++++++ packages/analytics-core/package.json | 4 ++-- packages/analytics-node-test/CHANGELOG.md | 9 +++++++++ packages/analytics-node-test/package.json | 4 ++-- packages/analytics-node/CHANGELOG.md | 9 +++++++++ packages/analytics-node/package.json | 6 +++--- packages/analytics-node/src/version.ts | 2 +- packages/analytics-react-native/CHANGELOG.md | 9 +++++++++ packages/analytics-react-native/package.json | 8 ++++---- packages/analytics-react-native/src/version.ts | 2 +- packages/analytics-types/CHANGELOG.md | 9 +++++++++ packages/analytics-types/package.json | 2 +- packages/marketing-analytics-browser/CHANGELOG.md | 9 +++++++++ packages/marketing-analytics-browser/README.md | 2 +- .../generated/amplitude-gtm-snippet.js | 4 ++-- .../generated/amplitude-snippet.js | 4 ++-- packages/marketing-analytics-browser/package.json | 14 +++++++------- .../marketing-analytics-browser/src/version.ts | 2 +- .../plugin-page-view-tracking-browser/CHANGELOG.md | 9 +++++++++ .../plugin-page-view-tracking-browser/package.json | 8 ++++---- .../plugin-web-attribution-browser/CHANGELOG.md | 9 +++++++++ .../plugin-web-attribution-browser/package.json | 8 ++++---- 32 files changed, 153 insertions(+), 51 deletions(-) diff --git a/packages/analytics-browser-test/CHANGELOG.md b/packages/analytics-browser-test/CHANGELOG.md index ce25fbe9e..754f26251 100644 --- a/packages/analytics-browser-test/CHANGELOG.md +++ b/packages/analytics-browser-test/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.4.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.4...@amplitude/analytics-browser-test@1.4.5) (2023-06-14) + +**Note:** Version bump only for package @amplitude/analytics-browser-test + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.4.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.3...@amplitude/analytics-browser-test@1.4.4) (2023-06-14) ### Bug Fixes diff --git a/packages/analytics-browser-test/package.json b/packages/analytics-browser-test/package.json index 413c6b862..914059eaf 100644 --- a/packages/analytics-browser-test/package.json +++ b/packages/analytics-browser-test/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser-test", - "version": "1.4.4", + "version": "1.4.5", "private": true, "description": "", "author": "Amplitude Inc", @@ -16,7 +16,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.11.1" + "@amplitude/analytics-browser": "^1.11.2" }, "devDependencies": { "nock": "^13.2.4" diff --git a/packages/analytics-browser/CHANGELOG.md b/packages/analytics-browser/CHANGELOG.md index 554ee24b8..646aea545 100644 --- a/packages/analytics-browser/CHANGELOG.md +++ b/packages/analytics-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.11.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.11.1...@amplitude/analytics-browser@1.11.2) (2023-06-14) + +**Note:** Version bump only for package @amplitude/analytics-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.11.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.11.0...@amplitude/analytics-browser@1.11.1) (2023-06-14) ### Bug Fixes diff --git a/packages/analytics-browser/README.md b/packages/analytics-browser/README.md index c248d306c..7332fe927 100644 --- a/packages/analytics-browser/README.md +++ b/packages/analytics-browser/README.md @@ -40,7 +40,7 @@ Alternatively, the package is also distributed through a CDN. Copy and paste the ```html diff --git a/packages/analytics-browser/generated/amplitude-snippet.js b/packages/analytics-browser/generated/amplitude-snippet.js index ac7bb52b9..c3d50c9ef 100644 --- a/packages/analytics-browser/generated/amplitude-snippet.js +++ b/packages/analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-So8wDvkfueWi1vtYoaT+2UMVdV5gytnrAUr8sumYyMQo4TNK4Lpct6VewTWsSNoH'; + as.integrity = 'sha384-bhExRHNdxBGFQ+AkmAurYWjqboez+iJTTaWsIubqgMeSgQZqjXMa8BI8890ijsCf'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.11.1-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.11.2-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/analytics-browser/package.json b/packages/analytics-browser/package.json index dcacb16a0..388ae14e8 100644 --- a/packages/analytics-browser/package.json +++ b/packages/analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser", - "version": "1.11.1", + "version": "1.11.2", "description": "Official Amplitude SDK for Web", "keywords": [ "analytics", @@ -44,11 +44,11 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.2", - "@amplitude/analytics-core": "^1.1.1", - "@amplitude/analytics-types": "^1.2.0", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.4", - "@amplitude/plugin-web-attribution-browser": "^1.0.4", + "@amplitude/analytics-client-common": "^1.0.3", + "@amplitude/analytics-core": "^1.1.2", + "@amplitude/analytics-types": "^1.2.1", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.5", + "@amplitude/plugin-web-attribution-browser": "^1.0.5", "@amplitude/ua-parser-js": "^0.7.31", "tslib": "^2.4.1" }, diff --git a/packages/analytics-browser/playground/amplitude.js b/packages/analytics-browser/playground/amplitude.js index 057e50752..9d8797e08 100644 --- a/packages/analytics-browser/playground/amplitude.js +++ b/packages/analytics-browser/playground/amplitude.js @@ -1 +1 @@ -!function(){"use strict";var e=function(t,i){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])},e(t,i)};function t(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=t}e(t,i),t.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var i=function(){return i=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!L(t,i))return!1}return!0},L=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&j(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return j(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},M=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||L(t,i)))},e}(),V=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},z=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},F=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return z(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),B="Event rejected due to exceeded retry count",$=function(e){return{promise:e||Promise.resolve()}},Q=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new F(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return $(this.dispatch(r))},e.prototype.identify=function(e,t){var i=V(e,t);return $(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return $(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new M;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return $(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return $(this.dispatch(n))},e.prototype.add=function(e){return this.config?$(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),$())},e.prototype.remove=function(e){return this.config?$(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),$())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(z(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,z(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=z(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return $(this.timeline.flush())},e}(),K=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return j(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),H="Amplitude Logger ",W=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=Y(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:return null===(s=o.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(s,e),[3,4]):("body"in s?this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(X(s))):this.fulfillRequest(e,s.statusCode,s.status),[2]);case 3:return a=o.sent(),u=(c=a)instanceof Error?c.message:String(c),this.config.loggerProvider.error(u),this.fulfillRequest(e,0,u),[3,4];case 4:return[2]}var c}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(X(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));d.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(z(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ne=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},re=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},oe=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},se=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=oe(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ae=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ue)},ce=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),le=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),de=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[D,t,e.substring(0,i)].filter(Boolean).join("_")},pe=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),fe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(le),ve="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},he={exports:{}};ee=he,te=he.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",q="Xiaomi",D="Zebra",A="Facebook",C=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},z=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?M(e,275):e,this},this.setUA(f),this};$.VERSION="0.7.31",$.BROWSER=C([a,l,"major"]),$.CPU=C([d]),$.DEVICE=C([s,c,u,p,f,h,v,g,b]),$.ENGINE=$.OS=C([a,l]),ee.exports&&(te=ee.exports=$),te.UAParser=$;var Q=typeof e!==n&&(e.jQuery||e.Zepto);if(Q&&!Q.ua){var K=new $;Q.ua=K.getResult(),Q.ua.get=function(){return K.getUA()},Q.ua.set=function(e){K.setUA(e);var t=K.getResult();for(var i in t)Q.ua[i]=t[i]}}}("object"==typeof window?window:ve);var ge=he.exports,be=function(){function e(){this.ua=new he.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:we(),platform:"Web",os:ye(this.ua),deviceModel:me(this.ua)}},e}(),ye=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},me=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},we=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},_e=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),Ie=function(){return Ie=Object.assign||function(e){for(var t,i=1,n=arguments.length;i=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(null!==(I=null!==(_=t.sessionId)&&void 0!==_?_:this.config.sessionId)&&void 0!==I?I:Date.now()),B=!0),($=Oe()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new ie).promise];case 11:return W.sent(),[4,this.add(new qe).promise];case 12:return W.sent(),[4,this.add(new xe).promise];case 13:return W.sent(),("boolean"==typeof(Z=this.config.defaultTracking)?Z:null==Z?void 0:Z.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(st,((n={})[ct]=o,n[lt]=i.pathname,n[dt]=e.id,n[pt]=e.text,n[ft]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,15];case 14:W.sent(),W.label=15;case 15:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),t.track(ot,((r={})[vt]=e.id,r[ht]=e.name,r[gt]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,17];case 16:W.sent(),W.label=17;case 17:return(null===(k=this.config.attribution)||void 0===k?void 0:k.disabled)?[3,19]:(Q=function(){for(var e,t,n=this,s=[],u=0;uthis.config.sessionTimeout&&this.setSessionId(i),[2,e.prototype.process.call(this,t)]}))}))},n}(Q),mt=function(){var e=new yt;return{init:ae(e.init.bind(e),"init",re(e),se(e,["config"])),add:ae(e.add.bind(e),"add",re(e),se(e,["config.apiKey","timeline.plugins"])),remove:ae(e.remove.bind(e),"remove",re(e),se(e,["config.apiKey","timeline.plugins"])),track:ae(e.track.bind(e),"track",re(e),se(e,["config.apiKey","timeline.queue.length"])),logEvent:ae(e.logEvent.bind(e),"logEvent",re(e),se(e,["config.apiKey","timeline.queue.length"])),identify:ae(e.identify.bind(e),"identify",re(e),se(e,["config.apiKey","timeline.queue.length"])),groupIdentify:ae(e.groupIdentify.bind(e),"groupIdentify",re(e),se(e,["config.apiKey","timeline.queue.length"])),setGroup:ae(e.setGroup.bind(e),"setGroup",re(e),se(e,["config.apiKey","timeline.queue.length"])),revenue:ae(e.revenue.bind(e),"revenue",re(e),se(e,["config.apiKey","timeline.queue.length"])),flush:ae(e.flush.bind(e),"flush",re(e),se(e,["config.apiKey","timeline.queue.length"])),getUserId:ae(e.getUserId.bind(e),"getUserId",re(e),se(e,["config","config.userId"])),setUserId:ae(e.setUserId.bind(e),"setUserId",re(e),se(e,["config","config.userId"])),getDeviceId:ae(e.getDeviceId.bind(e),"getDeviceId",re(e),se(e,["config","config.deviceId"])),setDeviceId:ae(e.setDeviceId.bind(e),"setDeviceId",re(e),se(e,["config","config.deviceId"])),reset:ae(e.reset.bind(e),"reset",re(e),se(e,["config","config.userId","config.deviceId"])),getSessionId:ae(e.getSessionId.bind(e),"getSessionId",re(e),se(e,["config"])),setSessionId:ae(e.setSessionId.bind(e),"setSessionId",re(e),se(e,["config"])),extendSession:ae(e.extendSession.bind(e),"extendSession",re(e),se(e,["config"])),setOptOut:ae(e.setOptOut.bind(e),"setOptOut",re(e),se(e,["config"])),setTransport:ae(e.setTransport.bind(e),"setTransport",re(e),se(e,["config"]))}},wt=mt(),_t=wt.add,It=wt.extendSession,kt=wt.flush,Et=wt.getDeviceId,St=wt.getSessionId,Tt=wt.getUserId,Ot=wt.groupIdentify,xt=wt.identify,Pt=wt.init,Rt=wt.logEvent,Nt=wt.remove,Ut=wt.reset,qt=wt.revenue,Dt=wt.setDeviceId,At=wt.setGroup,Ct=wt.setOptOut,jt=wt.setSessionId,Lt=wt.setTransport,Mt=wt.setUserId,Vt=wt.track,zt=Object.freeze({__proto__:null,add:_t,extendSession:It,flush:kt,getDeviceId:Et,getSessionId:St,getUserId:Tt,groupIdentify:Ot,identify:xt,init:Pt,logEvent:Rt,remove:Nt,reset:Ut,revenue:qt,setDeviceId:Dt,setGroup:At,setOptOut:Ct,setSessionId:jt,setTransport:Lt,setUserId:Mt,track:Vt,Types:q,createInstance:mt,runQueuedFunctions:Re,Revenue:K,Identify:M});!function(){var e=b();if(e){var t=function(e){var t=mt(),i=b();return i&&i.amplitude&&i.amplitude._iq&&e&&(i.amplitude._iq[e]=t),t};if(e.amplitude=Object.assign(e.amplitude||{},zt,{createInstance:t}),e.amplitude.invoked){var i=e.amplitude._q;e.amplitude._q=[],Re(zt,i);for(var n=Object.keys(e.amplitude._iq)||[],r=0;r0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!L(t,i))return!1}return!0},L=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&j(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return j(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},M=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||L(t,i)))},e}(),V=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},z=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},F=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return z(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),B="Event rejected due to exceeded retry count",$=function(e){return{promise:e||Promise.resolve()}},Q=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new F(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return $(this.dispatch(r))},e.prototype.identify=function(e,t){var i=V(e,t);return $(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return $(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new M;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return $(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return $(this.dispatch(n))},e.prototype.add=function(e){return this.config?$(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),$())},e.prototype.remove=function(e){return this.config?$(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),$())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(z(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,z(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=z(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return $(this.timeline.flush())},e}(),K=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return j(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),H="Amplitude Logger ",W=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=Y(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:return null===(s=o.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(s,e),[3,4]):("body"in s?this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(X(s))):this.fulfillRequest(e,s.statusCode,s.status),[2]);case 3:return a=o.sent(),u=(c=a)instanceof Error?c.message:String(c),this.config.loggerProvider.error(u),this.fulfillRequest(e,0,u),[3,4];case 4:return[2]}var c}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(X(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));d.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(z(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ne=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},re=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},oe=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},se=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=oe(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ae=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ue)},ce=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),le=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),de=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[D,t,e.substring(0,i)].filter(Boolean).join("_")},pe=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),fe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(le),ve="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},he={exports:{}};ee=he,te=he.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",q="Xiaomi",D="Zebra",A="Facebook",C=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},z=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?M(e,275):e,this},this.setUA(f),this};$.VERSION="0.7.31",$.BROWSER=C([a,l,"major"]),$.CPU=C([d]),$.DEVICE=C([s,c,u,p,f,h,v,g,b]),$.ENGINE=$.OS=C([a,l]),ee.exports&&(te=ee.exports=$),te.UAParser=$;var Q=typeof e!==n&&(e.jQuery||e.Zepto);if(Q&&!Q.ua){var K=new $;Q.ua=K.getResult(),Q.ua.get=function(){return K.getUA()},Q.ua.set=function(e){K.setUA(e);var t=K.getResult();for(var i in t)Q.ua[i]=t[i]}}}("object"==typeof window?window:ve);var ge=he.exports,be=function(){function e(){this.ua=new he.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:we(),platform:"Web",os:ye(this.ua),deviceModel:me(this.ua)}},e}(),ye=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},me=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},we=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},_e=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),Ie=function(){return Ie=Object.assign||function(e){for(var t,i=1,n=arguments.length;i=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(null!==(I=null!==(_=t.sessionId)&&void 0!==_?_:this.config.sessionId)&&void 0!==I?I:Date.now()),B=!0),($=Oe()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new ie).promise];case 11:return W.sent(),[4,this.add(new qe).promise];case 12:return W.sent(),[4,this.add(new xe).promise];case 13:return W.sent(),("boolean"==typeof(Z=this.config.defaultTracking)?Z:null==Z?void 0:Z.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(st,((n={})[ct]=o,n[lt]=i.pathname,n[dt]=e.id,n[pt]=e.text,n[ft]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,15];case 14:W.sent(),W.label=15;case 15:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),t.track(ot,((r={})[vt]=e.id,r[ht]=e.name,r[gt]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,17];case 16:W.sent(),W.label=17;case 17:return(null===(k=this.config.attribution)||void 0===k?void 0:k.disabled)?[3,19]:(Q=function(){for(var e,t,n=this,s=[],u=0;uthis.config.sessionTimeout&&this.setSessionId(i),[2,e.prototype.process.call(this,t)]}))}))},n}(Q),mt=function(){var e=new yt;return{init:ae(e.init.bind(e),"init",re(e),se(e,["config"])),add:ae(e.add.bind(e),"add",re(e),se(e,["config.apiKey","timeline.plugins"])),remove:ae(e.remove.bind(e),"remove",re(e),se(e,["config.apiKey","timeline.plugins"])),track:ae(e.track.bind(e),"track",re(e),se(e,["config.apiKey","timeline.queue.length"])),logEvent:ae(e.logEvent.bind(e),"logEvent",re(e),se(e,["config.apiKey","timeline.queue.length"])),identify:ae(e.identify.bind(e),"identify",re(e),se(e,["config.apiKey","timeline.queue.length"])),groupIdentify:ae(e.groupIdentify.bind(e),"groupIdentify",re(e),se(e,["config.apiKey","timeline.queue.length"])),setGroup:ae(e.setGroup.bind(e),"setGroup",re(e),se(e,["config.apiKey","timeline.queue.length"])),revenue:ae(e.revenue.bind(e),"revenue",re(e),se(e,["config.apiKey","timeline.queue.length"])),flush:ae(e.flush.bind(e),"flush",re(e),se(e,["config.apiKey","timeline.queue.length"])),getUserId:ae(e.getUserId.bind(e),"getUserId",re(e),se(e,["config","config.userId"])),setUserId:ae(e.setUserId.bind(e),"setUserId",re(e),se(e,["config","config.userId"])),getDeviceId:ae(e.getDeviceId.bind(e),"getDeviceId",re(e),se(e,["config","config.deviceId"])),setDeviceId:ae(e.setDeviceId.bind(e),"setDeviceId",re(e),se(e,["config","config.deviceId"])),reset:ae(e.reset.bind(e),"reset",re(e),se(e,["config","config.userId","config.deviceId"])),getSessionId:ae(e.getSessionId.bind(e),"getSessionId",re(e),se(e,["config"])),setSessionId:ae(e.setSessionId.bind(e),"setSessionId",re(e),se(e,["config"])),extendSession:ae(e.extendSession.bind(e),"extendSession",re(e),se(e,["config"])),setOptOut:ae(e.setOptOut.bind(e),"setOptOut",re(e),se(e,["config"])),setTransport:ae(e.setTransport.bind(e),"setTransport",re(e),se(e,["config"]))}},wt=mt(),_t=wt.add,It=wt.extendSession,kt=wt.flush,Et=wt.getDeviceId,St=wt.getSessionId,Tt=wt.getUserId,Ot=wt.groupIdentify,xt=wt.identify,Pt=wt.init,Rt=wt.logEvent,Nt=wt.remove,Ut=wt.reset,qt=wt.revenue,Dt=wt.setDeviceId,At=wt.setGroup,Ct=wt.setOptOut,jt=wt.setSessionId,Lt=wt.setTransport,Mt=wt.setUserId,Vt=wt.track,zt=Object.freeze({__proto__:null,add:_t,extendSession:It,flush:kt,getDeviceId:Et,getSessionId:St,getUserId:Tt,groupIdentify:Ot,identify:xt,init:Pt,logEvent:Rt,remove:Nt,reset:Ut,revenue:qt,setDeviceId:Dt,setGroup:At,setOptOut:Ct,setSessionId:jt,setTransport:Lt,setUserId:Mt,track:Vt,Types:q,createInstance:mt,runQueuedFunctions:Re,Revenue:K,Identify:M});!function(){var e=b();if(e){var t=function(e){var t=mt(),i=b();return i&&i.amplitude&&i.amplitude._iq&&e&&(i.amplitude._iq[e]=t),t};if(e.amplitude=Object.assign(e.amplitude||{},zt,{createInstance:t}),e.amplitude.invoked){var i=e.amplitude._q;e.amplitude._q=[],Re(zt,i);for(var n=Object.keys(e.amplitude._iq)||[],r=0;r ```html diff --git a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js index c0a12184b..8be97ab3d 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-/dfCBvYaIDAyRKZDCpZNiEv+OiiACVEqHLTnwo/90WCzmEptJAIC5hOf2ebqCyBY'; + as.integrity = 'sha384-GCneBnzsMTw+kmn6TwyFynia1fmqj+38TkwyYEyW8AkwF+Qw8IAh1CjqSgDYZ9X+'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.4-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.5-min.js.gz'; as.onload = function () { if (!window.amplitudeGTM.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/generated/amplitude-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-snippet.js index 246f31480..d4784919c 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-PsQ24m4bUh9DzHyRSZbvWu1el7OYz39YDvB+JssWCe3O/9X0TmapEHWZBvi+HMqo'; + as.integrity = 'sha384-ocQG0VTdcJGeNGgEnHLUASJ+2qacfMa5iU5EsLRXLdGs/g2XAPDUI4zdjSMlhO5U'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.4-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.5-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/package.json b/packages/marketing-analytics-browser/package.json index 4e9f68a3e..4d151e352 100644 --- a/packages/marketing-analytics-browser/package.json +++ b/packages/marketing-analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/marketing-analytics-browser", - "version": "1.0.4", + "version": "1.0.5", "description": "Official Amplitude SDK for Web and Marketing Analytics", "keywords": [ "analytics", @@ -43,12 +43,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.11.1", - "@amplitude/analytics-client-common": "^1.0.2", - "@amplitude/analytics-core": "^1.1.1", - "@amplitude/analytics-types": "^1.2.0", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.4", - "@amplitude/plugin-web-attribution-browser": "^1.0.4", + "@amplitude/analytics-browser": "^1.11.2", + "@amplitude/analytics-client-common": "^1.0.3", + "@amplitude/analytics-core": "^1.1.2", + "@amplitude/analytics-types": "^1.2.1", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.5", + "@amplitude/plugin-web-attribution-browser": "^1.0.5", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/marketing-analytics-browser/src/version.ts b/packages/marketing-analytics-browser/src/version.ts index be9db6827..6cec2cb92 100644 --- a/packages/marketing-analytics-browser/src/version.ts +++ b/packages/marketing-analytics-browser/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.0.4'; +export const VERSION = '1.0.5'; diff --git a/packages/plugin-page-view-tracking-browser/CHANGELOG.md b/packages/plugin-page-view-tracking-browser/CHANGELOG.md index 7118ce6ae..48be54e1c 100644 --- a/packages/plugin-page-view-tracking-browser/CHANGELOG.md +++ b/packages/plugin-page-view-tracking-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.4...@amplitude/plugin-page-view-tracking-browser@1.0.5) (2023-06-14) + +**Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.3...@amplitude/plugin-page-view-tracking-browser@1.0.4) (2023-06-14) **Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser diff --git a/packages/plugin-page-view-tracking-browser/package.json b/packages/plugin-page-view-tracking-browser/package.json index 245d00668..d77195f30 100644 --- a/packages/plugin-page-view-tracking-browser/package.json +++ b/packages/plugin-page-view-tracking-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-page-view-tracking-browser", - "version": "1.0.4", + "version": "1.0.5", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -37,12 +37,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.2", - "@amplitude/analytics-types": "^1.2.0", + "@amplitude/analytics-client-common": "^1.0.3", + "@amplitude/analytics-types": "^1.2.1", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.11.1", + "@amplitude/analytics-browser": "^1.11.2", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", diff --git a/packages/plugin-web-attribution-browser/CHANGELOG.md b/packages/plugin-web-attribution-browser/CHANGELOG.md index bba49a28f..909d34853 100644 --- a/packages/plugin-web-attribution-browser/CHANGELOG.md +++ b/packages/plugin-web-attribution-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.4...@amplitude/plugin-web-attribution-browser@1.0.5) (2023-06-14) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.3...@amplitude/plugin-web-attribution-browser@1.0.4) (2023-06-14) **Note:** Version bump only for package @amplitude/plugin-web-attribution-browser diff --git a/packages/plugin-web-attribution-browser/package.json b/packages/plugin-web-attribution-browser/package.json index f60c87a8f..3a1667300 100644 --- a/packages/plugin-web-attribution-browser/package.json +++ b/packages/plugin-web-attribution-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-web-attribution-browser", - "version": "1.0.4", + "version": "1.0.5", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -37,12 +37,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.2", - "@amplitude/analytics-types": "^1.2.0", + "@amplitude/analytics-client-common": "^1.0.3", + "@amplitude/analytics-types": "^1.2.1", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.11.1", + "@amplitude/analytics-browser": "^1.11.2", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", From 569464c698eb54b3da05e203ac90cf1a399d96ed Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Fri, 16 Jun 2023 14:51:56 -0700 Subject: [PATCH 020/214] feat: add option for instance name (#428) (#442) --- packages/analytics-browser/src/browser-client.ts | 6 +++--- packages/analytics-browser/test/config.test.ts | 3 +++ .../src/analytics-connector.ts | 12 ++++++------ .../analytics-client-common/src/plugins/identity.ts | 6 ++++-- .../test/plugins/identity.test.ts | 13 +++++++++---- packages/analytics-core/src/config.ts | 3 +++ packages/analytics-core/test/config.test.ts | 2 ++ packages/analytics-node/test/config.test.ts | 2 ++ packages/analytics-react-native/test/config.test.ts | 3 +++ packages/analytics-types/src/config/core.ts | 1 + 10 files changed, 36 insertions(+), 15 deletions(-) diff --git a/packages/analytics-browser/src/browser-client.ts b/packages/analytics-browser/src/browser-client.ts index 4215cd50d..dcb99ddaa 100644 --- a/packages/analytics-browser/src/browser-client.ts +++ b/packages/analytics-browser/src/browser-client.ts @@ -98,7 +98,7 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { // Set up the analytics connector to integrate with the experiment SDK. // Send events from the experiment SDK and forward identifies to the // identity store. - const connector = getAnalyticsConnector(); + const connector = getAnalyticsConnector(options.instanceName); connector.identityStore.setIdentity({ userId: this.config.userId, deviceId: this.config.deviceId, @@ -164,7 +164,7 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { } if (userId !== this.config.userId || userId === undefined) { this.config.userId = userId; - setConnectorUserId(userId); + setConnectorUserId(userId, this.config.instanceName); } } @@ -178,7 +178,7 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { return; } this.config.deviceId = deviceId; - setConnectorDeviceId(deviceId); + setConnectorDeviceId(deviceId, this.config.instanceName); } reset() { diff --git a/packages/analytics-browser/test/config.test.ts b/packages/analytics-browser/test/config.test.ts index ab583cbf2..71244c9bb 100644 --- a/packages/analytics-browser/test/config.test.ts +++ b/packages/analytics-browser/test/config.test.ts @@ -37,6 +37,7 @@ describe('config', () => { flushIntervalMillis: 1000, flushMaxRetries: 5, flushQueueSize: 30, + instanceName: '$default_instance', loggerProvider: logger, logLevel: LogLevel.Warn, minIdLength: undefined, @@ -83,6 +84,7 @@ describe('config', () => { flushIntervalMillis: 1000, flushMaxRetries: 5, flushQueueSize: 30, + instanceName: '$default_instance', loggerProvider: logger, logLevel: LogLevel.Warn, minIdLength: undefined, @@ -153,6 +155,7 @@ describe('config', () => { flushIntervalMillis: 1000, flushMaxRetries: 5, flushQueueSize: 30, + instanceName: '$default_instance', _lastEventId: 100, _lastEventTime: 1, loggerProvider: logger, diff --git a/packages/analytics-client-common/src/analytics-connector.ts b/packages/analytics-client-common/src/analytics-connector.ts index a2766d902..4d2fb87bb 100644 --- a/packages/analytics-client-common/src/analytics-connector.ts +++ b/packages/analytics-client-common/src/analytics-connector.ts @@ -1,15 +1,15 @@ import { AnalyticsConnector } from '@amplitude/analytics-connector'; -export const getAnalyticsConnector = (): AnalyticsConnector => { - return AnalyticsConnector.getInstance('$default_instance'); +export const getAnalyticsConnector = (instanceName = '$default_instance'): AnalyticsConnector => { + return AnalyticsConnector.getInstance(instanceName); }; -export const setConnectorUserId = (userId: string | undefined): void => { +export const setConnectorUserId = (userId: string | undefined, instanceName?: string): void => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - getAnalyticsConnector().identityStore.editIdentity().setUserId(userId).commit(); + getAnalyticsConnector(instanceName).identityStore.editIdentity().setUserId(userId).commit(); }; -export const setConnectorDeviceId = (deviceId: string): void => { - getAnalyticsConnector().identityStore.editIdentity().setDeviceId(deviceId).commit(); +export const setConnectorDeviceId = (deviceId: string, instanceName?: string): void => { + getAnalyticsConnector(instanceName).identityStore.editIdentity().setDeviceId(deviceId).commit(); }; diff --git a/packages/analytics-client-common/src/plugins/identity.ts b/packages/analytics-client-common/src/plugins/identity.ts index 14a58a91c..3104047a2 100644 --- a/packages/analytics-client-common/src/plugins/identity.ts +++ b/packages/analytics-client-common/src/plugins/identity.ts @@ -15,7 +15,9 @@ export class IdentityEventSender implements BeforePlugin { return context; } - setup(_: Config): Promise { - return Promise.resolve(undefined); + async setup(config: Config) { + if (config.instanceName) { + this.identityStore = getAnalyticsConnector(config.instanceName).identityStore; + } } } diff --git a/packages/analytics-client-common/test/plugins/identity.test.ts b/packages/analytics-client-common/test/plugins/identity.test.ts index 8cc07b3aa..1e005d860 100644 --- a/packages/analytics-client-common/test/plugins/identity.test.ts +++ b/packages/analytics-client-common/test/plugins/identity.test.ts @@ -8,7 +8,7 @@ describe('identity', () => { getAnalyticsConnector().identityStore.setIdentity({ userProperties: {} }); }); - test('should set identity in analytics connector on identify', async () => { + test('should set identity in analytics connector on identify with default instance', async () => { const plugin = new IdentityEventSender(); await plugin.setup({} as Config); const event = { @@ -17,14 +17,17 @@ describe('identity', () => { $set: { k: 'v' }, }, }; - void (await plugin.execute(event)); + const result = await plugin.execute(event); const identity = getAnalyticsConnector().identityStore.getIdentity(); + expect(result).toEqual(event); expect(identity.userProperties).toEqual({ k: 'v' }); }); - test('should not modify event on identify', async () => { + test('should set identity in analytics connector on identify with instance name', async () => { const plugin = new IdentityEventSender(); - await plugin.setup({} as Config); + await plugin.setup({ + instanceName: 'env', + } as Config); const event = { event_type: '$identify', user_properties: { @@ -32,7 +35,9 @@ describe('identity', () => { }, }; const result = await plugin.execute(event); + const identity = getAnalyticsConnector('env').identityStore.getIdentity(); expect(result).toEqual(event); + expect(identity.userProperties).toEqual({ k: 'v' }); }); test('should do nothing on track event', async () => { diff --git a/packages/analytics-core/src/config.ts b/packages/analytics-core/src/config.ts index e2a3e86d0..b77aaa1f3 100644 --- a/packages/analytics-core/src/config.ts +++ b/packages/analytics-core/src/config.ts @@ -23,6 +23,7 @@ export const getDefaultConfig = () => ({ flushMaxRetries: 12, flushQueueSize: 200, flushIntervalMillis: 10000, + instanceName: '$default_instance', logLevel: LogLevel.Warn, loggerProvider: new Logger(), optOut: false, @@ -36,6 +37,7 @@ export class Config implements IConfig { flushIntervalMillis: number; flushMaxRetries: number; flushQueueSize: number; + instanceName?: string; loggerProvider: ILogger; logLevel: LogLevel; minIdLength?: number; @@ -61,6 +63,7 @@ export class Config implements IConfig { this.flushIntervalMillis = options.flushIntervalMillis ?? defaultConfig.flushIntervalMillis; this.flushMaxRetries = options.flushMaxRetries || defaultConfig.flushMaxRetries; this.flushQueueSize = options.flushQueueSize || defaultConfig.flushQueueSize; + this.instanceName = options.instanceName || defaultConfig.instanceName; this.loggerProvider = options.loggerProvider || defaultConfig.loggerProvider; this.logLevel = options.logLevel ?? defaultConfig.logLevel; this.minIdLength = options.minIdLength; diff --git a/packages/analytics-core/test/config.test.ts b/packages/analytics-core/test/config.test.ts index 8becab36f..6903b27dd 100644 --- a/packages/analytics-core/test/config.test.ts +++ b/packages/analytics-core/test/config.test.ts @@ -22,6 +22,7 @@ describe('config', () => { flushIntervalMillis: 10000, flushMaxRetries: 12, flushQueueSize: 200, + instanceName: '$default_instance', logLevel: LogLevel.Warn, loggerProvider: new Logger(), minIdLength: undefined, @@ -58,6 +59,7 @@ describe('config', () => { flushIntervalMillis: 10000, flushMaxRetries: 12, flushQueueSize: 200, + instanceName: '$default_instance', logLevel: LogLevel.Verbose, loggerProvider: new Logger(), minIdLength: undefined, diff --git a/packages/analytics-node/test/config.test.ts b/packages/analytics-node/test/config.test.ts index e755ab424..ebe0e65f5 100644 --- a/packages/analytics-node/test/config.test.ts +++ b/packages/analytics-node/test/config.test.ts @@ -16,6 +16,7 @@ describe('config', () => { flushIntervalMillis: 10000, flushMaxRetries: 12, flushQueueSize: 200, + instanceName: '$default_instance', loggerProvider: logger, logLevel: LogLevel.Warn, _optOut: false, @@ -41,6 +42,7 @@ describe('config', () => { flushIntervalMillis: 10000, flushMaxRetries: 12, flushQueueSize: 200, + instanceName: '$default_instance', loggerProvider: logger, logLevel: LogLevel.Warn, _optOut: false, diff --git a/packages/analytics-react-native/test/config.test.ts b/packages/analytics-react-native/test/config.test.ts index e939b82f4..458ba04d6 100644 --- a/packages/analytics-react-native/test/config.test.ts +++ b/packages/analytics-react-native/test/config.test.ts @@ -34,6 +34,7 @@ describe('config', () => { flushIntervalMillis: 1000, flushMaxRetries: 5, flushQueueSize: 30, + instanceName: '$default_instance', loggerProvider: logger, logLevel: LogLevel.Warn, minIdLength: undefined, @@ -83,6 +84,7 @@ describe('config', () => { flushIntervalMillis: 1000, flushMaxRetries: 5, flushQueueSize: 30, + instanceName: '$default_instance', loggerProvider: logger, logLevel: LogLevel.Warn, minIdLength: undefined, @@ -152,6 +154,7 @@ describe('config', () => { flushIntervalMillis: 1000, flushMaxRetries: 5, flushQueueSize: 30, + instanceName: '$default_instance', _lastEventId: 2, _lastEventTime: 1, loggerProvider: logger, diff --git a/packages/analytics-types/src/config/core.ts b/packages/analytics-types/src/config/core.ts index 8e4171f47..20486b20c 100644 --- a/packages/analytics-types/src/config/core.ts +++ b/packages/analytics-types/src/config/core.ts @@ -11,6 +11,7 @@ export interface Config { flushIntervalMillis: number; flushMaxRetries: number; flushQueueSize: number; + instanceName?: string; logLevel: LogLevel; loggerProvider: Logger; minIdLength?: number; From b31226dc0e10641a47511b619961f190e096517a Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Fri, 16 Jun 2023 14:57:03 -0700 Subject: [PATCH 021/214] fix: types for plugins to be env specific (#440) (#441) --- .../analytics-types/src/client/node-client.ts | 22 +++++++++- .../analytics-types/src/client/web-client.ts | 41 ++++++++++++++++++- packages/analytics-types/src/plugin.ts | 14 +++---- 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/packages/analytics-types/src/client/node-client.ts b/packages/analytics-types/src/client/node-client.ts index 22fc39bb6..a1b3d6899 100644 --- a/packages/analytics-types/src/client/node-client.ts +++ b/packages/analytics-types/src/client/node-client.ts @@ -1,6 +1,7 @@ import { AmplitudeReturn } from '../amplitude-promise'; -import { NodeOptions } from '../config'; +import { NodeConfig, NodeOptions } from '../config'; import { CoreClient } from './core-client'; +import { Plugin } from '../plugin'; export interface NodeClient extends CoreClient { /** @@ -12,4 +13,23 @@ export interface NodeClient extends CoreClient { * ``` */ init(apiKey: string, options?: NodeOptions): AmplitudeReturn; + + /** + * Adds a new plugin. + * + * ```typescript + * const plugin = { + * name: 'my-plugin', + * type: 'enrichment', + * async setup(config: NodeConfig, amplitude: NodeClient) { + * return; + * }, + * async execute(event: Event) { + * return event; + * }, + * }; + * amplitude.add(plugin); + * ``` + */ + add(plugin: Plugin): AmplitudeReturn; } diff --git a/packages/analytics-types/src/client/web-client.ts b/packages/analytics-types/src/client/web-client.ts index 731678bf0..385a6c13d 100644 --- a/packages/analytics-types/src/client/web-client.ts +++ b/packages/analytics-types/src/client/web-client.ts @@ -1,7 +1,8 @@ import { AmplitudeReturn } from '../amplitude-promise'; -import { BrowserOptions, ReactNativeOptions } from '../config'; +import { BrowserConfig, BrowserOptions, ReactNativeConfig, ReactNativeOptions } from '../config'; import { TransportType } from '../transport'; import { CoreClient } from './core-client'; +import { Plugin } from '../plugin'; interface Client extends CoreClient { /** @@ -116,6 +117,25 @@ export interface BrowserClient extends Client { * ``` */ setTransport(transport: TransportType): void; + + /** + * Adds a new plugin. + * + * ```typescript + * const plugin = { + * name: 'my-plugin', + * type: 'enrichment', + * async setup(config: BrowserConfig, amplitude: BrowserClient) { + * return; + * }, + * async execute(event: Event) { + * return event; + * }, + * }; + * amplitude.add(plugin); + * ``` + */ + add(plugin: Plugin): AmplitudeReturn; } export interface ReactNativeClient extends Client { @@ -128,4 +148,23 @@ export interface ReactNativeClient extends Client { * ``` */ init(apiKey: string, userId?: string, options?: ReactNativeOptions): AmplitudeReturn; + + /** + * Adds a new plugin. + * + * ```typescript + * const plugin = { + * name: 'my-plugin', + * type: 'enrichment', + * async setup(config: ReactNativeConfig, amplitude: ReactNativeClient) { + * return; + * }, + * async execute(event: Event) { + * return event; + * }, + * }; + * amplitude.add(plugin); + * ``` + */ + add(plugin: Plugin): AmplitudeReturn; } diff --git a/packages/analytics-types/src/plugin.ts b/packages/analytics-types/src/plugin.ts index c1e58f5e0..b40801223 100644 --- a/packages/analytics-types/src/plugin.ts +++ b/packages/analytics-types/src/plugin.ts @@ -9,26 +9,26 @@ export enum PluginType { DESTINATION = 'destination', } -export interface BeforePlugin { +export interface BeforePlugin { name: string; type: PluginType.BEFORE; - setup(config: Config, client?: T): Promise; + setup(config: U, client?: T): Promise; execute(context: Event): Promise; } -export interface EnrichmentPlugin { +export interface EnrichmentPlugin { name: string; type: PluginType.ENRICHMENT; - setup(config: Config, client?: T): Promise; + setup(config: U, client?: T): Promise; execute(context: Event): Promise; } -export interface DestinationPlugin { +export interface DestinationPlugin { name: string; type: PluginType.DESTINATION; - setup(config: Config, client?: T): Promise; + setup(config: U, client?: T): Promise; execute(context: Event): Promise; flush?(): Promise; } -export type Plugin = BeforePlugin | EnrichmentPlugin | DestinationPlugin; +export type Plugin = BeforePlugin | EnrichmentPlugin | DestinationPlugin; From ed299baca27a7eaa00f0dbc50b4e82d86aea51d9 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Tue, 30 May 2023 12:37:14 -0400 Subject: [PATCH 022/214] feat(plugins): mvp of session replay plugin --- packages/plugin-session-replay/CHANGELOG.md | 363 ++++++++++++++++++ packages/plugin-session-replay/README.md | 100 +++++ packages/plugin-session-replay/jest.config.js | 10 + packages/plugin-session-replay/package.json | 57 +++ .../plugin-session-replay/rollup.config.js | 6 + packages/plugin-session-replay/src/helpers.ts | 0 packages/plugin-session-replay/src/index.ts | 2 + .../src/session-replay.ts | 71 ++++ .../src/typings/session-replay.ts | 8 + .../plugin-session-replay/tsconfig.es5.json | 10 + .../plugin-session-replay/tsconfig.esm.json | 10 + packages/plugin-session-replay/tsconfig.json | 11 + yarn.lock | 14 +- 13 files changed, 654 insertions(+), 8 deletions(-) create mode 100644 packages/plugin-session-replay/CHANGELOG.md create mode 100644 packages/plugin-session-replay/README.md create mode 100644 packages/plugin-session-replay/jest.config.js create mode 100644 packages/plugin-session-replay/package.json create mode 100644 packages/plugin-session-replay/rollup.config.js create mode 100644 packages/plugin-session-replay/src/helpers.ts create mode 100644 packages/plugin-session-replay/src/index.ts create mode 100644 packages/plugin-session-replay/src/session-replay.ts create mode 100644 packages/plugin-session-replay/src/typings/session-replay.ts create mode 100644 packages/plugin-session-replay/tsconfig.es5.json create mode 100644 packages/plugin-session-replay/tsconfig.esm.json create mode 100644 packages/plugin-session-replay/tsconfig.json diff --git a/packages/plugin-session-replay/CHANGELOG.md b/packages/plugin-session-replay/CHANGELOG.md new file mode 100644 index 000000000..2666c3996 --- /dev/null +++ b/packages/plugin-session-replay/CHANGELOG.md @@ -0,0 +1,363 @@ +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.7.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.7...@amplitude/plugin-web-attribution-browser@0.7.0) (2023-05-04) + +### Features + +- add attribution tracking for linkedin click id li_fat_id + ([ca81f3d](https://github.com/amplitude/Amplitude-TypeScript/commit/ca81f3d75ece7e0e23a1bc1b6889107d53a60a86)) +- add rtd_cid for Reddit campaign tracking/attribution + ([784e080](https://github.com/amplitude/Amplitude-TypeScript/commit/784e080aa129c37e850d7f34115beb9770044e4e)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.6.7](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.6...@amplitude/plugin-web-attribution-browser@0.6.7) (2023-04-27) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.6.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.5...@amplitude/plugin-web-attribution-browser@0.6.6) (2023-04-27) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.6.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.4...@amplitude/plugin-web-attribution-browser@0.6.5) (2023-04-25) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.6.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.3...@amplitude/plugin-web-attribution-browser@0.6.4) (2023-04-06) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.6.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.2...@amplitude/plugin-web-attribution-browser@0.6.3) (2023-03-31) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.6.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.2-beta.0...@amplitude/plugin-web-attribution-browser@0.6.2) (2023-03-31) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.6.2-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.1...@amplitude/plugin-web-attribution-browser@0.6.2-beta.0) (2023-03-31) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.6.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.1-beta.1...@amplitude/plugin-web-attribution-browser@0.6.1) (2023-03-03) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.6.1-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.1-beta.0...@amplitude/plugin-web-attribution-browser@0.6.1-beta.1) (2023-03-03) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.6.1-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.0...@amplitude/plugin-web-attribution-browser@0.6.1-beta.0) (2023-03-03) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.6.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.0-beta.4...@amplitude/plugin-web-attribution-browser@0.6.0) (2023-02-27) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.6.0-beta.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.0-beta.3...@amplitude/plugin-web-attribution-browser@0.6.0-beta.4) (2023-02-27) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.6.0-beta.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.0-beta.2...@amplitude/plugin-web-attribution-browser@0.6.0-beta.3) (2023-02-26) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.6.0-beta.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.0-beta.1...@amplitude/plugin-web-attribution-browser@0.6.0-beta.2) (2023-02-25) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.6.0-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.0-beta.0...@amplitude/plugin-web-attribution-browser@0.6.0-beta.1) (2023-02-24) + +### Bug Fixes + +- improper cookie usage ([#330](https://github.com/amplitude/Amplitude-TypeScript/issues/330)) + ([e670091](https://github.com/amplitude/Amplitude-TypeScript/commit/e670091e59014bb35bd9b3ec2a7192f259393575)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.6.0-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.7...@amplitude/plugin-web-attribution-browser@0.6.0-beta.0) (2023-02-24) + +### Bug Fixes + +- remove client parameter requirement for page view tracking plugin + ([#329](https://github.com/amplitude/Amplitude-TypeScript/issues/329)) + ([1e01575](https://github.com/amplitude/Amplitude-TypeScript/commit/1e015750b52880ca63afa5162cb482995f04d1c6)) + +### Features + +- pass amplitude instance to plugin.setup for enhanced plugin capabilities + ([#328](https://github.com/amplitude/Amplitude-TypeScript/issues/328)) + ([91eeaa0](https://github.com/amplitude/Amplitude-TypeScript/commit/91eeaa0d6bff6bde39538bb54548a938df784462)) +- retrofit web attribution and page view plugins to browser SDK + ([#331](https://github.com/amplitude/Amplitude-TypeScript/issues/331)) + ([ba845d3](https://github.com/amplitude/Amplitude-TypeScript/commit/ba845d3329bd6bebe3b89f24f4f316088c2d62b9)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.5.7](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.6...@amplitude/plugin-web-attribution-browser@0.5.7) (2023-02-09) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.5.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.5...@amplitude/plugin-web-attribution-browser@0.5.6) (2023-02-02) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.5.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.5-beta.0...@amplitude/plugin-web-attribution-browser@0.5.5) (2023-01-31) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.5.5-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.4...@amplitude/plugin-web-attribution-browser@0.5.5-beta.0) (2023-01-26) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.5.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.3...@amplitude/plugin-web-attribution-browser@0.5.4) (2023-01-11) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.5.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.2...@amplitude/plugin-web-attribution-browser@0.5.3) (2022-12-21) + +### Bug Fixes + +- upgrade dependencies to resolve dependabot vulnerability alerts + ([#299](https://github.com/amplitude/Amplitude-TypeScript/issues/299)) + ([7dd1cd1](https://github.com/amplitude/Amplitude-TypeScript/commit/7dd1cd1b23a71981a4ad90b4b30cc9b7d28c4412)) + +### Reverts + +- Revert "Updated dependencies" + ([7ca9964](https://github.com/amplitude/Amplitude-TypeScript/commit/7ca9964781e4b900c6c027bdddf2ae1e7428ba18)) + +## [0.5.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.1...@amplitude/plugin-web-attribution-browser@0.5.2) (2022-12-06) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +## [0.5.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.0...@amplitude/plugin-web-attribution-browser@0.5.1) (2022-12-05) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# [0.5.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.4.2...@amplitude/plugin-web-attribution-browser@0.5.0) (2022-11-28) + +### Features + +- add utm_id tracking ([#284](https://github.com/amplitude/Amplitude-TypeScript/issues/284)) + ([f72dcf1](https://github.com/amplitude/Amplitude-TypeScript/commit/f72dcf1788ebc84544aaee1dc41b1d1ba6e4c06e)) + +## [0.4.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.4.1...@amplitude/plugin-web-attribution-browser@0.4.2) (2022-11-22) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +## [0.4.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.4.0...@amplitude/plugin-web-attribution-browser@0.4.1) (2022-11-15) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# [0.4.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.3.0...@amplitude/plugin-web-attribution-browser@0.4.0) (2022-11-01) + +### Features + +- ignore subdomains when comparing newness of campaigns + ([#260](https://github.com/amplitude/Amplitude-TypeScript/issues/260)) + ([8bb2b76](https://github.com/amplitude/Amplitude-TypeScript/commit/8bb2b76faf37783a58e953391468bd31c089e3a3)) + +# [0.3.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.5...@amplitude/plugin-web-attribution-browser@0.3.0) (2022-11-01) + +### Features + +- enhance logger with debug information ([#254](https://github.com/amplitude/Amplitude-TypeScript/issues/254)) + ([5c6175e](https://github.com/amplitude/Amplitude-TypeScript/commit/5c6175e9372cbeea264ddb34c6cc68148063d4f7)) + +## [0.2.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.4...@amplitude/plugin-web-attribution-browser@0.2.5) (2022-10-30) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +## [0.2.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.3...@amplitude/plugin-web-attribution-browser@0.2.4) (2022-10-26) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +## [0.2.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.3-beta.1...@amplitude/plugin-web-attribution-browser@0.2.3) (2022-10-25) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +## [0.2.3-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.3-beta.0...@amplitude/plugin-web-attribution-browser@0.2.3-beta.1) (2022-10-25) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +## [0.2.3-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.2...@amplitude/plugin-web-attribution-browser@0.2.3-beta.0) (2022-10-25) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +## [0.2.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.1...@amplitude/plugin-web-attribution-browser@0.2.2) (2022-10-25) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +## [0.2.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.0...@amplitude/plugin-web-attribution-browser@0.2.1) (2022-10-14) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# [0.2.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.6...@amplitude/plugin-web-attribution-browser@0.2.0) (2022-10-04) + +### Features + +- add gbraid and wbraid as campaign parameters ([#242](https://github.com/amplitude/Amplitude-TypeScript/issues/242)) + ([514b7cd](https://github.com/amplitude/Amplitude-TypeScript/commit/514b7cdea9fee0c4e61479b087f7acdfea889350)) + +## [0.1.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.5...@amplitude/plugin-web-attribution-browser@0.1.6) (2022-09-30) + +### Bug Fixes + +- resolve web attribution is not tracking the first direct/organic traffic + ([#239](https://github.com/amplitude/Amplitude-TypeScript/issues/239)) + ([98a3363](https://github.com/amplitude/Amplitude-TypeScript/commit/98a33633a7a6de7ee147c8cbf690e5546ce53163)) + +## [0.1.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.4...@amplitude/plugin-web-attribution-browser@0.1.5) (2022-09-30) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +## [0.1.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.4-beta.2...@amplitude/plugin-web-attribution-browser@0.1.4) (2022-09-28) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +## [0.1.4-beta.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.4-beta.1...@amplitude/plugin-web-attribution-browser@0.1.4-beta.2) (2022-09-27) + +### Bug Fixes + +- js script export name for marketing analytics plugins + ([aa7b05c](https://github.com/amplitude/Amplitude-TypeScript/commit/aa7b05cb192e23924081a363f3567573f76a3b62)) + +## [0.1.4-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.4-beta.0...@amplitude/plugin-web-attribution-browser@0.1.4-beta.1) (2022-09-27) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +## [0.1.4-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.3...@amplitude/plugin-web-attribution-browser@0.1.4-beta.0) (2022-09-26) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +## [0.1.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.2...@amplitude/plugin-web-attribution-browser@0.1.3) (2022-09-26) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +## [0.1.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.1...@amplitude/plugin-web-attribution-browser@0.1.2) (2022-09-26) + +### Bug Fixes + +- update base config to include additional click ids + ([#229](https://github.com/amplitude/Amplitude-TypeScript/issues/229)) + ([5596931](https://github.com/amplitude/Amplitude-TypeScript/commit/55969310714c43f138e1702ba285fd4dadcdcb44)) + +## [0.1.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.0...@amplitude/plugin-web-attribution-browser@0.1.1) (2022-09-22) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# 0.1.0 (2022-09-16) + +### Features + +- new marketing analytics plugin ([#213](https://github.com/amplitude/Amplitude-TypeScript/issues/213)) + ([02ff174](https://github.com/amplitude/Amplitude-TypeScript/commit/02ff174e3361173dbf15ed3acf72e950810e174f)) diff --git a/packages/plugin-session-replay/README.md b/packages/plugin-session-replay/README.md new file mode 100644 index 000000000..2cffd9ced --- /dev/null +++ b/packages/plugin-session-replay/README.md @@ -0,0 +1,100 @@ +

+ + + +
+

+ +# @amplitude/plugin-web-attribution-browser + +Official Browser SDK plugin for web attribution tracking + +## Installation + +This package is published on NPM registry and is available to be installed using npm and yarn. + +```sh +# npm +npm install @amplitude/plugin-web-attribution-browser + +# yarn +yarn add @amplitude/plugin-web-attribution-browser +``` + +## Usage + +This plugin works on top of Amplitude Browser SDK and adds web attribution tracking features to built-in features. To use this plugin, you need to install `@amplitude/analytics-browser` version `v1.4.0` or later. + +### 1. Import Amplitude packages + +* `@amplitude/analytics-browser` +* `@amplitude/plugin-web-attribution-browser` + +```typescript +import * as amplitude from '@amplitude/analytics-browser'; +import { webAttributionPlugin } from '@amplitude/plugin-web-attribution-browser'; +``` + +### 2. Instantiate page view plugin + +The plugin requires 1 parameter, which is the `amplitude` instance. The plugin also accepts an optional second parameter, which is an `Object` to configure the plugin based on your use case. + +```typescript +const webAttributionTracking = webAttributionPlugin(amplitude, { + disabled: undefined, + excludeReferrers: undefined, + initialEmptyValue: undefined, + resetSessionOnNewCampaign: undefined, +}); +``` + +#### Options + +|Name|Type|Default|Description| +|-|-|-|-| +|`disabled`|`boolean`|`false`|Use this option to enable or disable web attribution tracking. By default, upon installing this plugin, web attribution tracking is enabled.| +|`excludeReferrers`|`string[]`|`[]`|Use this option to prevent the plugin from tracking campaigns parameters from specific referrers. For example: `subdomain.domain.com`.| +|`initialEmptyValue`|`string`|`"EMPTY"`|Use this option to specify empty values for [first-touch attribution](https://www.docs.developers.amplitude.com/data/sdks/marketing-analytics-browser/#first-touch-attribution).| +|`resetSessionOnNewCampaign`|`boolean`|`false`|Use this option to control whether a new session should start on a new campaign.| + +### 3. Install plugin to Amplitude SDK + +```typescript +amplitude.add(webAttributionTracking); +``` + +### 4. Initialize Amplitude SDK + +```typescript +amplitude.init('API_KEY'); +``` + +## Resulting web attribution event + +This plugin tracks campaign parameters based on your configuration. A web attribution event is composed of the following values: + +#### Event type +* `"$idenfity"` + +#### User properties + +|Property|Description| +|-|-| +|`utm_source`|URL query parameter value for `utm_source`| +|`utm_medium`|URL query parameter value for `utm_medium`| +|`utm_campaign`|URL query parameter value for `utm_campaign`| +|`utm_term`|URL query parameter value for `utm_term`| +|`utm_content`|URL query parameter value for `utm_content`| +|`referrer`|Referring webstite or `document.referrer`| +|`referring_domain`|Referring website's domain, including subdomain| +|`dclid`|URL query parameter value for `dclid`| +|`gbraid`|URL query parameter value for `gbraid`| +|`gclid`|URL query parameter value for `gclid`| +|`fbclid`|URL query parameter value for `fbclid`| +|`ko_click_id`|URL query parameter value for `ko_click_id`| +|`li_fat_id`|URL query parameter value for `li_fat_id`| +|`msclkid`|URL query parameter value for `msclkid`| +|`rtd_cid`|URL query parameter value for `rtd_cid`| +|`ttclid`|URL query parameter value for `ttclid`| +|`twclid`|URL query parameter value for `twclid`| +|`wbraid`|URL query parameter value for `wbraid`| diff --git a/packages/plugin-session-replay/jest.config.js b/packages/plugin-session-replay/jest.config.js new file mode 100644 index 000000000..dc4094b18 --- /dev/null +++ b/packages/plugin-session-replay/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../jest.config.js'); +const package = require('./package'); + +module.exports = { + ...baseConfig, + displayName: package.name, + rootDir: '.', + testEnvironment: 'jsdom', + coveragePathIgnorePatterns: ['index.ts'], +}; diff --git a/packages/plugin-session-replay/package.json b/packages/plugin-session-replay/package.json new file mode 100644 index 000000000..d83cb4bc2 --- /dev/null +++ b/packages/plugin-session-replay/package.json @@ -0,0 +1,57 @@ +{ + "name": "@amplitude/plugin-session-replay", + "version": "1.0.0-beta.0", + "description": "", + "author": "Amplitude Inc", + "homepage": "https://github.com/amplitude/Amplitude-TypeScript", + "license": "MIT", + "main": "lib/cjs/index.js", + "module": "lib/esm/index.js", + "types": "lib/esm/index.d.ts", + "sideEffects": false, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/amplitude/Amplitude-TypeScript.git" + }, + "scripts": { + "build": "yarn bundle && yarn build:es5 && yarn build:esm", + "bundle": "rollup --config rollup.config.js", + "build:es5": "tsc -p ./tsconfig.es5.json", + "build:esm": "tsc -p ./tsconfig.esm.json", + "clean": "rimraf node_modules lib coverage", + "fix": "yarn fix:eslint & yarn fix:prettier", + "fix:eslint": "eslint '{src,test}/**/*.ts' --fix", + "fix:prettier": "prettier --write \"{src,test}/**/*.ts\"", + "lint": "yarn lint:eslint & yarn lint:prettier", + "lint:eslint": "eslint '{src,test}/**/*.ts'", + "lint:prettier": "prettier --check \"{src,test}/**/*.ts\"", + "publish": "node ../../scripts/publish/upload-to-s3.js", + "test": "jest", + "typecheck": "tsc -p ./tsconfig.json" + }, + "bugs": { + "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" + }, + "dependencies": { + "@amplitude/analytics-client-common": "^1.0.0-beta.0", + "@amplitude/analytics-types": "^1.0.0-beta.0", + "rrweb": "^2.0.0-alpha.4", + "tslib": "^2.4.1" + }, + "devDependencies": { + "@amplitude/analytics-browser": "^1.10.3", + "@rollup/plugin-commonjs": "^23.0.4", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^10.0.1", + "rollup": "^2.79.1", + "rollup-plugin-execute": "^1.1.1", + "rollup-plugin-gzip": "^3.1.0", + "rollup-plugin-terser": "^7.0.2" + }, + "files": [ + "lib" + ] +} diff --git a/packages/plugin-session-replay/rollup.config.js b/packages/plugin-session-replay/rollup.config.js new file mode 100644 index 000000000..8fbd9d329 --- /dev/null +++ b/packages/plugin-session-replay/rollup.config.js @@ -0,0 +1,6 @@ +import { iife, umd } from '../../scripts/build/rollup.config'; + +iife.input = umd.input; +iife.output.name = 'sessionReplay'; + +export default [umd, iife]; diff --git a/packages/plugin-session-replay/src/helpers.ts b/packages/plugin-session-replay/src/helpers.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugin-session-replay/src/index.ts b/packages/plugin-session-replay/src/index.ts new file mode 100644 index 000000000..779a4865c --- /dev/null +++ b/packages/plugin-session-replay/src/index.ts @@ -0,0 +1,2 @@ +export { sessionReplayPlugin as plugin, sessionReplayPlugin } from './session-replay'; +export * as Types from './typings/session-replay'; diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts new file mode 100644 index 000000000..adda3751e --- /dev/null +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -0,0 +1,71 @@ +import { BrowserClient, BrowserConfig, EnrichmentPlugin, Event, PluginType } from '@amplitude/analytics-types'; +import { record } from 'rrweb'; +import { CreateSessionReplayPlugin, Options } from './typings/session-replay'; + +export const SESSION_REPLAY_SERVER_URL = 'https://api2.amplitude.com/sessions/track'; + +export const sessionReplayPlugin: CreateSessionReplayPlugin = function (options: Options = {}) { + let amplitudeConfig: BrowserConfig | undefined; + // let amplitude: BrowserClient | undefined; + + options = { + ...options, + }; + + let events: string[] = []; + + const plugin: EnrichmentPlugin = { + name: '@amplitude/plugin-session-replay', + type: PluginType.ENRICHMENT, + + setup: async function (config: BrowserConfig, client: BrowserClient) { + console.log('in setup', 'config', config, 'client', client); + amplitudeConfig = config; + + config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); + }, + + execute: async (event: Event) => { + console.log('event', event); + // todo: this should be a constant/type + if (event.event_type === 'session_start') { + record({ + emit(event) { + events.push(JSON.stringify(event)); + }, + maskAllInputs: true, + }); + } + + if (event.event_type === 'session_end' && events.length) { + const payload = { + api_key: amplitudeConfig?.apiKey, + device_id: amplitudeConfig?.deviceId, + session_id: amplitudeConfig?.sessionId, + start_timestamp: amplitudeConfig?.sessionId, + events_batch: { + version: 1, + events, + seq_number: 1, + }, + }; + + const options: RequestInit = { + headers: { + 'Content-Type': 'application/json', + Accept: '*/*', + }, + body: JSON.stringify(payload), + method: 'POST', + }; + events = []; + const res = await fetch(SESSION_REPLAY_SERVER_URL, options); + console.log('res', res); + } + + return event; + }, + }; + + return plugin; +}; diff --git a/packages/plugin-session-replay/src/typings/session-replay.ts b/packages/plugin-session-replay/src/typings/session-replay.ts new file mode 100644 index 000000000..cc9f7bd26 --- /dev/null +++ b/packages/plugin-session-replay/src/typings/session-replay.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/no-empty-interface */ +import { EnrichmentPlugin } from '@amplitude/analytics-types'; + +export interface Options {} + +export interface CreateSessionReplayPlugin { + (options?: Options): EnrichmentPlugin; +} diff --git a/packages/plugin-session-replay/tsconfig.es5.json b/packages/plugin-session-replay/tsconfig.es5.json new file mode 100644 index 000000000..77e041d3f --- /dev/null +++ b/packages/plugin-session-replay/tsconfig.es5.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"], + "compilerOptions": { + "module": "commonjs", + "noEmit": false, + "outDir": "lib/cjs", + "rootDir": "./src" + } +} diff --git a/packages/plugin-session-replay/tsconfig.esm.json b/packages/plugin-session-replay/tsconfig.esm.json new file mode 100644 index 000000000..bec981eee --- /dev/null +++ b/packages/plugin-session-replay/tsconfig.esm.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"], + "compilerOptions": { + "module": "es6", + "noEmit": false, + "outDir": "lib/esm", + "rootDir": "./src" + } +} diff --git a/packages/plugin-session-replay/tsconfig.json b/packages/plugin-session-replay/tsconfig.json new file mode 100644 index 000000000..955dcce78 --- /dev/null +++ b/packages/plugin-session-replay/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*", "test/**/*"], + "compilerOptions": { + "baseUrl": ".", + "esModuleInterop": true, + "lib": ["dom"], + "noEmit": true, + "rootDir": ".", + } +} diff --git a/yarn.lock b/yarn.lock index ecd604a1f..33a296cca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,14 +2,12 @@ # yarn lockfile v1 -"@amplitude/analytics-connector@^1.4.5": - version "1.4.6" - resolved "https://registry.yarnpkg.com/@amplitude/analytics-connector/-/analytics-connector-1.4.6.tgz#60a66eaf0bcdcd17db4f414ff340c69e63116a72" - integrity sha512-6jD2pOosRD4y8DT8StUCz7yTd5ZDkdOU9/AWnlWKM5qk90Mz7sdZrdZ9H7sA/L3yOJEpQOYZgQplQdWWUzyWug== - dependencies: - "@amplitude/ua-parser-js" "0.7.31" +"@amplitude/analytics-connector@^1.4.8": + version "1.4.8" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-connector/-/analytics-connector-1.4.8.tgz#dd801303db2662bc51be7e0194eeb8bd72267c42" + integrity sha512-dFW7c7Wb6Ng7vbmzwbaXZSpqfBx37ukamJV9ErFYYS8vGZK/Hkbt3M7fZHBI4WFU6CCwakr2ZXPme11uGPYWkQ== -"@amplitude/ua-parser-js@0.7.31", "@amplitude/ua-parser-js@^0.7.31": +"@amplitude/ua-parser-js@^0.7.31": version "0.7.31" resolved "https://registry.yarnpkg.com/@amplitude/ua-parser-js/-/ua-parser-js-0.7.31.tgz#749bf7cb633cfcc7ff3c10805bad7c5f6fbdbc61" integrity sha512-+z8UGRaj13Pt5NDzOnkTBy49HE2CX64jeL0ArB86HAtilpnfkPB7oqkigN7Lf2LxscMg4QhFD7mmCfedh3rqTg== @@ -9561,7 +9559,7 @@ nocache@^3.0.1: resolved "https://registry.yarnpkg.com/nocache/-/nocache-3.0.4.tgz#5b37a56ec6e09fc7d401dceaed2eab40c8bfdf79" integrity sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw== -nock@^13.2.4, nock@^13.2.9: +nock@^13.2.4: version "13.2.9" resolved "https://registry.yarnpkg.com/nock/-/nock-13.2.9.tgz#4faf6c28175d36044da4cfa68e33e5a15086ad4c" integrity sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA== From c88d85ae4c041f05064d2e703b9cb0a0edd008f7 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Wed, 31 May 2023 10:29:44 -0400 Subject: [PATCH 023/214] feat(plugins): implemented retry logic for session replay --- packages/plugin-session-replay/CHANGELOG.md | 352 ------------------ packages/plugin-session-replay/src/helpers.ts | 15 + packages/plugin-session-replay/src/index.ts | 2 +- .../plugin-session-replay/src/messages.ts | 7 + .../src/session-replay.ts | 230 +++++++++--- .../src/typings/session-replay.ts | 10 + 6 files changed, 206 insertions(+), 410 deletions(-) create mode 100644 packages/plugin-session-replay/src/messages.ts diff --git a/packages/plugin-session-replay/CHANGELOG.md b/packages/plugin-session-replay/CHANGELOG.md index 2666c3996..b67c38fc7 100644 --- a/packages/plugin-session-replay/CHANGELOG.md +++ b/packages/plugin-session-replay/CHANGELOG.md @@ -3,358 +3,6 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -# [0.7.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.7...@amplitude/plugin-web-attribution-browser@0.7.0) (2023-05-04) - -### Features - -- add attribution tracking for linkedin click id li_fat_id - ([ca81f3d](https://github.com/amplitude/Amplitude-TypeScript/commit/ca81f3d75ece7e0e23a1bc1b6889107d53a60a86)) -- add rtd_cid for Reddit campaign tracking/attribution - ([784e080](https://github.com/amplitude/Amplitude-TypeScript/commit/784e080aa129c37e850d7f34115beb9770044e4e)) - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.6.7](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.6...@amplitude/plugin-web-attribution-browser@0.6.7) (2023-04-27) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.6.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.5...@amplitude/plugin-web-attribution-browser@0.6.6) (2023-04-27) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.6.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.4...@amplitude/plugin-web-attribution-browser@0.6.5) (2023-04-25) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.6.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.3...@amplitude/plugin-web-attribution-browser@0.6.4) (2023-04-06) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.6.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.2...@amplitude/plugin-web-attribution-browser@0.6.3) (2023-03-31) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.6.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.2-beta.0...@amplitude/plugin-web-attribution-browser@0.6.2) (2023-03-31) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.6.2-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.1...@amplitude/plugin-web-attribution-browser@0.6.2-beta.0) (2023-03-31) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.6.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.1-beta.1...@amplitude/plugin-web-attribution-browser@0.6.1) (2023-03-03) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.6.1-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.1-beta.0...@amplitude/plugin-web-attribution-browser@0.6.1-beta.1) (2023-03-03) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.6.1-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.0...@amplitude/plugin-web-attribution-browser@0.6.1-beta.0) (2023-03-03) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# [0.6.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.0-beta.4...@amplitude/plugin-web-attribution-browser@0.6.0) (2023-02-27) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# [0.6.0-beta.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.0-beta.3...@amplitude/plugin-web-attribution-browser@0.6.0-beta.4) (2023-02-27) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# [0.6.0-beta.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.0-beta.2...@amplitude/plugin-web-attribution-browser@0.6.0-beta.3) (2023-02-26) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# [0.6.0-beta.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.0-beta.1...@amplitude/plugin-web-attribution-browser@0.6.0-beta.2) (2023-02-25) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# [0.6.0-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.6.0-beta.0...@amplitude/plugin-web-attribution-browser@0.6.0-beta.1) (2023-02-24) - -### Bug Fixes - -- improper cookie usage ([#330](https://github.com/amplitude/Amplitude-TypeScript/issues/330)) - ([e670091](https://github.com/amplitude/Amplitude-TypeScript/commit/e670091e59014bb35bd9b3ec2a7192f259393575)) - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# [0.6.0-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.7...@amplitude/plugin-web-attribution-browser@0.6.0-beta.0) (2023-02-24) - -### Bug Fixes - -- remove client parameter requirement for page view tracking plugin - ([#329](https://github.com/amplitude/Amplitude-TypeScript/issues/329)) - ([1e01575](https://github.com/amplitude/Amplitude-TypeScript/commit/1e015750b52880ca63afa5162cb482995f04d1c6)) - -### Features - -- pass amplitude instance to plugin.setup for enhanced plugin capabilities - ([#328](https://github.com/amplitude/Amplitude-TypeScript/issues/328)) - ([91eeaa0](https://github.com/amplitude/Amplitude-TypeScript/commit/91eeaa0d6bff6bde39538bb54548a938df784462)) -- retrofit web attribution and page view plugins to browser SDK - ([#331](https://github.com/amplitude/Amplitude-TypeScript/issues/331)) - ([ba845d3](https://github.com/amplitude/Amplitude-TypeScript/commit/ba845d3329bd6bebe3b89f24f4f316088c2d62b9)) - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.5.7](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.6...@amplitude/plugin-web-attribution-browser@0.5.7) (2023-02-09) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.5.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.5...@amplitude/plugin-web-attribution-browser@0.5.6) (2023-02-02) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.5.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.5-beta.0...@amplitude/plugin-web-attribution-browser@0.5.5) (2023-01-31) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.5.5-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.4...@amplitude/plugin-web-attribution-browser@0.5.5-beta.0) (2023-01-26) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.5.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.3...@amplitude/plugin-web-attribution-browser@0.5.4) (2023-01-11) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.5.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.2...@amplitude/plugin-web-attribution-browser@0.5.3) (2022-12-21) - -### Bug Fixes - -- upgrade dependencies to resolve dependabot vulnerability alerts - ([#299](https://github.com/amplitude/Amplitude-TypeScript/issues/299)) - ([7dd1cd1](https://github.com/amplitude/Amplitude-TypeScript/commit/7dd1cd1b23a71981a4ad90b4b30cc9b7d28c4412)) - -### Reverts - -- Revert "Updated dependencies" - ([7ca9964](https://github.com/amplitude/Amplitude-TypeScript/commit/7ca9964781e4b900c6c027bdddf2ae1e7428ba18)) - -## [0.5.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.1...@amplitude/plugin-web-attribution-browser@0.5.2) (2022-12-06) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -## [0.5.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.5.0...@amplitude/plugin-web-attribution-browser@0.5.1) (2022-12-05) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# [0.5.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.4.2...@amplitude/plugin-web-attribution-browser@0.5.0) (2022-11-28) - -### Features - -- add utm_id tracking ([#284](https://github.com/amplitude/Amplitude-TypeScript/issues/284)) - ([f72dcf1](https://github.com/amplitude/Amplitude-TypeScript/commit/f72dcf1788ebc84544aaee1dc41b1d1ba6e4c06e)) - -## [0.4.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.4.1...@amplitude/plugin-web-attribution-browser@0.4.2) (2022-11-22) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -## [0.4.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.4.0...@amplitude/plugin-web-attribution-browser@0.4.1) (2022-11-15) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# [0.4.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.3.0...@amplitude/plugin-web-attribution-browser@0.4.0) (2022-11-01) - -### Features - -- ignore subdomains when comparing newness of campaigns - ([#260](https://github.com/amplitude/Amplitude-TypeScript/issues/260)) - ([8bb2b76](https://github.com/amplitude/Amplitude-TypeScript/commit/8bb2b76faf37783a58e953391468bd31c089e3a3)) - -# [0.3.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.5...@amplitude/plugin-web-attribution-browser@0.3.0) (2022-11-01) - -### Features - -- enhance logger with debug information ([#254](https://github.com/amplitude/Amplitude-TypeScript/issues/254)) - ([5c6175e](https://github.com/amplitude/Amplitude-TypeScript/commit/5c6175e9372cbeea264ddb34c6cc68148063d4f7)) - -## [0.2.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.4...@amplitude/plugin-web-attribution-browser@0.2.5) (2022-10-30) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -## [0.2.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.3...@amplitude/plugin-web-attribution-browser@0.2.4) (2022-10-26) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -## [0.2.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.3-beta.1...@amplitude/plugin-web-attribution-browser@0.2.3) (2022-10-25) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -## [0.2.3-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.3-beta.0...@amplitude/plugin-web-attribution-browser@0.2.3-beta.1) (2022-10-25) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -## [0.2.3-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.2...@amplitude/plugin-web-attribution-browser@0.2.3-beta.0) (2022-10-25) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -## [0.2.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.1...@amplitude/plugin-web-attribution-browser@0.2.2) (2022-10-25) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -## [0.2.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.2.0...@amplitude/plugin-web-attribution-browser@0.2.1) (2022-10-14) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -# [0.2.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.6...@amplitude/plugin-web-attribution-browser@0.2.0) (2022-10-04) - -### Features - -- add gbraid and wbraid as campaign parameters ([#242](https://github.com/amplitude/Amplitude-TypeScript/issues/242)) - ([514b7cd](https://github.com/amplitude/Amplitude-TypeScript/commit/514b7cdea9fee0c4e61479b087f7acdfea889350)) - -## [0.1.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.5...@amplitude/plugin-web-attribution-browser@0.1.6) (2022-09-30) - -### Bug Fixes - -- resolve web attribution is not tracking the first direct/organic traffic - ([#239](https://github.com/amplitude/Amplitude-TypeScript/issues/239)) - ([98a3363](https://github.com/amplitude/Amplitude-TypeScript/commit/98a33633a7a6de7ee147c8cbf690e5546ce53163)) - -## [0.1.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.4...@amplitude/plugin-web-attribution-browser@0.1.5) (2022-09-30) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -## [0.1.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.4-beta.2...@amplitude/plugin-web-attribution-browser@0.1.4) (2022-09-28) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -## [0.1.4-beta.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.4-beta.1...@amplitude/plugin-web-attribution-browser@0.1.4-beta.2) (2022-09-27) - -### Bug Fixes - -- js script export name for marketing analytics plugins - ([aa7b05c](https://github.com/amplitude/Amplitude-TypeScript/commit/aa7b05cb192e23924081a363f3567573f76a3b62)) - -## [0.1.4-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.4-beta.0...@amplitude/plugin-web-attribution-browser@0.1.4-beta.1) (2022-09-27) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -## [0.1.4-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.3...@amplitude/plugin-web-attribution-browser@0.1.4-beta.0) (2022-09-26) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -## [0.1.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.2...@amplitude/plugin-web-attribution-browser@0.1.3) (2022-09-26) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - -## [0.1.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.1...@amplitude/plugin-web-attribution-browser@0.1.2) (2022-09-26) - -### Bug Fixes - -- update base config to include additional click ids - ([#229](https://github.com/amplitude/Amplitude-TypeScript/issues/229)) - ([5596931](https://github.com/amplitude/Amplitude-TypeScript/commit/55969310714c43f138e1702ba285fd4dadcdcb44)) - -## [0.1.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@0.1.0...@amplitude/plugin-web-attribution-browser@0.1.1) (2022-09-22) - -**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser - # 0.1.0 (2022-09-16) ### Features diff --git a/packages/plugin-session-replay/src/helpers.ts b/packages/plugin-session-replay/src/helpers.ts index e69de29bb..de5a7638d 100644 --- a/packages/plugin-session-replay/src/helpers.ts +++ b/packages/plugin-session-replay/src/helpers.ts @@ -0,0 +1,15 @@ +// Creates an array of elements split into groups the length of size. +// If array can't be split evenly, the final chunk will be the remaining elements. +// Works similary as https://lodash.com/docs/4.17.15#chunk + +export const chunk = (arr: T[], size: number) => { + const chunkSize = Math.max(size, 1); + return arr.reduce((chunks, element, index) => { + const chunkIndex = Math.floor(index / chunkSize); + if (!chunks[chunkIndex]) { + chunks[chunkIndex] = []; + } + chunks[chunkIndex].push(element); + return chunks; + }, []); +}; diff --git a/packages/plugin-session-replay/src/index.ts b/packages/plugin-session-replay/src/index.ts index 779a4865c..4a1605e03 100644 --- a/packages/plugin-session-replay/src/index.ts +++ b/packages/plugin-session-replay/src/index.ts @@ -1,2 +1,2 @@ -export { sessionReplayPlugin as plugin, sessionReplayPlugin } from './session-replay'; +export { SessionReplayPlugin as Plugin, SessionReplayPlugin } from './session-replay'; export * as Types from './typings/session-replay'; diff --git a/packages/plugin-session-replay/src/messages.ts b/packages/plugin-session-replay/src/messages.ts new file mode 100644 index 000000000..adce6761f --- /dev/null +++ b/packages/plugin-session-replay/src/messages.ts @@ -0,0 +1,7 @@ +export const SUCCESS_MESSAGE = 'Session replay event batch tracked successfully'; +export const UNEXPECTED_ERROR_MESSAGE = 'Unexpected error occurred'; +export const MAX_RETRIES_EXCEEDED_MESSAGE = 'Session replay event batch rejected due to exceeded retry count'; +export const OPT_OUT_MESSAGE = 'Event skipped due to optOut config'; +export const MISSING_API_KEY_MESSAGE = 'Session replay event batch rejected due to missing API key'; +export const INVALID_API_KEY = 'Invalid API key'; +export const CLIENT_NOT_INITIALIZED = 'Client not initialized'; diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts index adda3751e..d51e7b784 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -1,71 +1,187 @@ -import { BrowserClient, BrowserConfig, EnrichmentPlugin, Event, PluginType } from '@amplitude/analytics-types'; +import { BaseTransport } from '@amplitude/analytics-core'; +import { BrowserConfig, EnrichmentPlugin, Event, PluginType, Status } from '@amplitude/analytics-types'; import { record } from 'rrweb'; -import { CreateSessionReplayPlugin, Options } from './typings/session-replay'; +import { MAX_RETRIES_EXCEEDED_MESSAGE, MISSING_API_KEY_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from './messages'; +import { SessionReplayContext } from './typings/session-replay'; export const SESSION_REPLAY_SERVER_URL = 'https://api2.amplitude.com/sessions/track'; -export const sessionReplayPlugin: CreateSessionReplayPlugin = function (options: Options = {}) { - let amplitudeConfig: BrowserConfig | undefined; - // let amplitude: BrowserClient | undefined; +export class SessionReplayPlugin implements EnrichmentPlugin { + name = '@amplitude/plugin-session-replay'; + type = PluginType.ENRICHMENT as const; + // this.config is defined in setup() which will always be called first + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + config: BrowserConfig; + retryTimeout = 1000; + events: string[][] = []; + private scheduled: ReturnType | null = null; + queue: SessionReplayContext[] = []; - options = { - ...options, - }; + async setup(config: BrowserConfig) { + console.log('setup called', config); + this.events = []; + this.config = config; - let events: string[] = []; + config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); + } - const plugin: EnrichmentPlugin = { - name: '@amplitude/plugin-session-replay', - type: PluginType.ENRICHMENT, - - setup: async function (config: BrowserConfig, client: BrowserClient) { - console.log('in setup', 'config', config, 'client', client); - amplitudeConfig = config; + async execute(event: Event) { + // console.log('event', event); + // todo: this should be a constant/type + if (event.event_type === 'session_start') { + this.events.push([]); + console.log('starting new session', this.events); + record({ + emit: (event) => { + console.log('events in emit', this.events); + this.events[this.events.length - 1].push(JSON.stringify(event)); + }, + maskAllInputs: true, + }); + } else if (event.event_type === 'session_end' && this.events.length) { + console.log('ending session', this.events); + try { + this.addToQueue({ + events: this.events[this.events.length - 1], + index: this.events.length - 1, + attempts: 0, + timeout: 0, + }); + } catch (e) { + this.config.loggerProvider.error(e); + } + } - config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); - }, + return event; + } - execute: async (event: Event) => { - console.log('event', event); - // todo: this should be a constant/type - if (event.event_type === 'session_start') { - record({ - emit(event) { - events.push(JSON.stringify(event)); - }, - maskAllInputs: true, - }); + addToQueue(...list: SessionReplayContext[]) { + const tryable = list.filter((context) => { + console.log('this.config.flushMaxRetries', this.config.flushMaxRetries); + if (context.attempts < this.config.flushMaxRetries) { + context.attempts += 1; + return true; } + throw new Error(`${MAX_RETRIES_EXCEEDED_MESSAGE}, batch index, ${context.index}`); + }); + console.log('tryable items', tryable); + tryable.forEach((context) => { + this.queue = this.queue.concat(context); + if (context.timeout === 0) { + this.schedule(this.config.flushIntervalMillis); + return; + } + + setTimeout(() => { + context.timeout = 0; + this.schedule(0); + }, context.timeout); + }); + + // this.saveEvents(); + } - if (event.event_type === 'session_end' && events.length) { - const payload = { - api_key: amplitudeConfig?.apiKey, - device_id: amplitudeConfig?.deviceId, - session_id: amplitudeConfig?.sessionId, - start_timestamp: amplitudeConfig?.sessionId, - events_batch: { - version: 1, - events, - seq_number: 1, - }, - }; - - const options: RequestInit = { - headers: { - 'Content-Type': 'application/json', - Accept: '*/*', - }, - body: JSON.stringify(payload), - method: 'POST', - }; - events = []; - const res = await fetch(SESSION_REPLAY_SERVER_URL, options); - console.log('res', res); + schedule(timeout: number) { + if (this.scheduled) return; + this.scheduled = setTimeout(() => { + void this.flush(true).then(() => { + if (this.queue.length > 0) { + this.schedule(timeout); + } + }); + }, timeout); + } + + async flush(useRetry = false) { + const list: SessionReplayContext[] = []; + const later: SessionReplayContext[] = []; + this.queue.forEach((context) => (context.timeout === 0 ? list.push(context) : later.push(context))); + this.queue = later; + + if (this.scheduled) { + clearTimeout(this.scheduled); + this.scheduled = null; + } + + // const batches = chunk(list, this.config.flushQueueSize); // todo do we need to chunk + try { + await Promise.all(list.map((context) => this.send(context, useRetry))); + } catch (e) { + this.config.loggerProvider.error(e); + } + } + + async send(context: SessionReplayContext, useRetry = true) { + if (!this.config.apiKey) { + return Promise.reject(new Error(MISSING_API_KEY_MESSAGE)); + } + const payload = { + api_key: this.config.apiKey, + device_id: this.config.deviceId, + session_id: this.config.sessionId, + start_timestamp: this.config.sessionId, + events_batch: { + version: 1, + events: context.events, + seq_number: context.index, + }, + }; + try { + const options: RequestInit = { + headers: { + 'Content-Type': 'application/json', + Accept: '*/*', + }, + body: JSON.stringify(payload), + method: 'POST', + }; + const res = await fetch(SESSION_REPLAY_SERVER_URL, options); + console.log('res', res); + if (res === null) { + return Promise.reject(new Error(UNEXPECTED_ERROR_MESSAGE)); } + if (!useRetry) { + let responseBody = ''; + try { + responseBody = JSON.stringify(res.body, null, 2); + } catch { + // to avoid crash, but don't care about the error, add comment to avoid empty block lint error + } + return Promise.resolve(`${res.status}: ${responseBody}`); + } + return this.handleReponse(res, context); + } catch (e) { + return Promise.reject(e); + } + } + + async handleReponse(res: Response, context: SessionReplayContext) { + const { status } = res; + const parsedStatus = new BaseTransport().buildStatus(status); + switch (parsedStatus) { + case Status.Success: + return this.handleSuccessResponse(res); + break; + + default: + return this.handleOtherResponse(context); + } + } - return event; - }, - }; + async handleSuccessResponse(res: Response) { + return Promise.resolve(`${res.status}`); + } - return plugin; -}; + async handleOtherResponse(context: SessionReplayContext) { + try { + this.addToQueue({ + ...context, + timeout: context.attempts * this.retryTimeout, + }); + } catch (e) { + return Promise.reject(new Error(e as string)); + } + return Promise.resolve(`Retrying batch at index ${context.index}`); + } +} diff --git a/packages/plugin-session-replay/src/typings/session-replay.ts b/packages/plugin-session-replay/src/typings/session-replay.ts index cc9f7bd26..f6cb6dd17 100644 --- a/packages/plugin-session-replay/src/typings/session-replay.ts +++ b/packages/plugin-session-replay/src/typings/session-replay.ts @@ -6,3 +6,13 @@ export interface Options {} export interface CreateSessionReplayPlugin { (options?: Options): EnrichmentPlugin; } + +export type Events = string[]; + +export interface SessionReplayContext { + events: Events; + index: number; + attempts: number; + timeout: number; + //todo callback? +} From e85de00d868f88c85245b17311755f0fc315c513 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Wed, 31 May 2023 11:28:01 -0400 Subject: [PATCH 024/214] feat(plugins): batching requests at max list size --- packages/plugin-session-replay/src/helpers.ts | 14 +++++++ .../src/session-replay.ts | 42 +++++++++++-------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/packages/plugin-session-replay/src/helpers.ts b/packages/plugin-session-replay/src/helpers.ts index de5a7638d..9303df6d1 100644 --- a/packages/plugin-session-replay/src/helpers.ts +++ b/packages/plugin-session-replay/src/helpers.ts @@ -1,3 +1,17 @@ +const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events +const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; +// const MAX_EVENT_LIST_SIZE_IN_BYTES = 350000 + +export const shouldSplitEventsList = (eventsList: string[], nextEventString: string): boolean => { + const sizeOfNextEvent = new Blob([nextEventString]).size; + const sizeOfEventsList = new Blob(eventsList).size; + console.log('sizeOfEventsList', sizeOfEventsList); + if (sizeOfEventsList + sizeOfNextEvent >= MAX_EVENT_LIST_SIZE_IN_BYTES) { + return true; + } + return false; +}; + // Creates an array of elements split into groups the length of size. // If array can't be split evenly, the final chunk will be the remaining elements. // Works similary as https://lodash.com/docs/4.17.15#chunk diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts index d51e7b784..c15bcbc21 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -1,6 +1,7 @@ import { BaseTransport } from '@amplitude/analytics-core'; import { BrowserConfig, EnrichmentPlugin, Event, PluginType, Status } from '@amplitude/analytics-types'; import { record } from 'rrweb'; +import { shouldSplitEventsList } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, MISSING_API_KEY_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from './messages'; import { SessionReplayContext } from './typings/session-replay'; @@ -20,45 +21,52 @@ export class SessionReplayPlugin implements EnrichmentPlugin { async setup(config: BrowserConfig) { console.log('setup called', config); - this.events = []; + this.events = [[]]; this.config = config; config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); } async execute(event: Event) { - // console.log('event', event); // todo: this should be a constant/type if (event.event_type === 'session_start') { - this.events.push([]); + this.events = [[]]; console.log('starting new session', this.events); record({ emit: (event) => { - console.log('events in emit', this.events); - this.events[this.events.length - 1].push(JSON.stringify(event)); + const eventString = JSON.stringify(event); + const shouldSplit = shouldSplitEventsList(this.events[this.events.length - 1], eventString); + if (shouldSplit) { + this.sendEventsList(this.events[this.events.length - 1], this.events.length - 1); + this.events.push([]); + } + this.events[this.events.length - 1].push(eventString); }, maskAllInputs: true, }); } else if (event.event_type === 'session_end' && this.events.length) { console.log('ending session', this.events); - try { - this.addToQueue({ - events: this.events[this.events.length - 1], - index: this.events.length - 1, - attempts: 0, - timeout: 0, - }); - } catch (e) { - this.config.loggerProvider.error(e); - } + this.sendEventsList(this.events[this.events.length - 1], this.events.length - 1); } return event; } + sendEventsList(events: string[], index: number) { + try { + this.addToQueue({ + events, + index, + attempts: 0, + timeout: 0, + }); + } catch (e) { + this.config.loggerProvider.error(e); + } + } + addToQueue(...list: SessionReplayContext[]) { const tryable = list.filter((context) => { - console.log('this.config.flushMaxRetries', this.config.flushMaxRetries); if (context.attempts < this.config.flushMaxRetries) { context.attempts += 1; return true; @@ -78,8 +86,6 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.schedule(0); }, context.timeout); }); - - // this.saveEvents(); } schedule(timeout: number) { From 07889568f4f8e269825db077393c34857a2ccdfe Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 1 Jun 2023 12:36:52 -0400 Subject: [PATCH 025/214] feat(plugins): implementing indexeddb storage for session replay --- packages/plugin-session-replay/package.json | 1 + packages/plugin-session-replay/src/helpers.ts | 7 +- .../plugin-session-replay/src/messages.ts | 1 + .../src/session-replay.ts | 226 ++++++++++++------ .../src/typings/session-replay.ts | 11 +- yarn.lock | 5 + 6 files changed, 176 insertions(+), 75 deletions(-) diff --git a/packages/plugin-session-replay/package.json b/packages/plugin-session-replay/package.json index d83cb4bc2..be02e5817 100644 --- a/packages/plugin-session-replay/package.json +++ b/packages/plugin-session-replay/package.json @@ -38,6 +38,7 @@ "dependencies": { "@amplitude/analytics-client-common": "^1.0.0-beta.0", "@amplitude/analytics-types": "^1.0.0-beta.0", + "idb-keyval": "^6.2.1", "rrweb": "^2.0.0-alpha.4", "tslib": "^2.4.1" }, diff --git a/packages/plugin-session-replay/src/helpers.ts b/packages/plugin-session-replay/src/helpers.ts index 9303df6d1..49e7ae1ae 100644 --- a/packages/plugin-session-replay/src/helpers.ts +++ b/packages/plugin-session-replay/src/helpers.ts @@ -1,11 +1,10 @@ -const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events -const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; -// const MAX_EVENT_LIST_SIZE_IN_BYTES = 350000 +// const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events +// const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; +const MAX_EVENT_LIST_SIZE_IN_BYTES = 80000; export const shouldSplitEventsList = (eventsList: string[], nextEventString: string): boolean => { const sizeOfNextEvent = new Blob([nextEventString]).size; const sizeOfEventsList = new Blob(eventsList).size; - console.log('sizeOfEventsList', sizeOfEventsList); if (sizeOfEventsList + sizeOfNextEvent >= MAX_EVENT_LIST_SIZE_IN_BYTES) { return true; } diff --git a/packages/plugin-session-replay/src/messages.ts b/packages/plugin-session-replay/src/messages.ts index adce6761f..898cdc023 100644 --- a/packages/plugin-session-replay/src/messages.ts +++ b/packages/plugin-session-replay/src/messages.ts @@ -5,3 +5,4 @@ export const OPT_OUT_MESSAGE = 'Event skipped due to optOut config'; export const MISSING_API_KEY_MESSAGE = 'Session replay event batch rejected due to missing API key'; export const INVALID_API_KEY = 'Invalid API key'; export const CLIENT_NOT_INITIALIZED = 'Client not initialized'; +export const STORAGE_FAILURE = 'Failed to store session replay events in IndexedDB'; diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts index c15bcbc21..8cfea7772 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -1,12 +1,13 @@ -import { BaseTransport } from '@amplitude/analytics-core'; +import { AMPLITUDE_PREFIX, BaseTransport } from '@amplitude/analytics-core'; import { BrowserConfig, EnrichmentPlugin, Event, PluginType, Status } from '@amplitude/analytics-types'; +import { get, update } from 'idb-keyval'; import { record } from 'rrweb'; import { shouldSplitEventsList } from './helpers'; -import { MAX_RETRIES_EXCEEDED_MESSAGE, MISSING_API_KEY_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from './messages'; -import { SessionReplayContext } from './typings/session-replay'; - -export const SESSION_REPLAY_SERVER_URL = 'https://api2.amplitude.com/sessions/track'; +import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, UNEXPECTED_ERROR_MESSAGE } from './messages'; +import { Events, IDBStore, SessionReplayContext } from './typings/session-replay'; +const SESSION_REPLAY_SERVER_URL = 'https://api2.amplitude.com/sessions/track'; +const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; export class SessionReplayPlugin implements EnrichmentPlugin { name = '@amplitude/plugin-session-replay'; type = PluginType.ENRICHMENT as const; @@ -14,66 +15,104 @@ export class SessionReplayPlugin implements EnrichmentPlugin { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore config: BrowserConfig; + storageKey = ''; retryTimeout = 1000; - events: string[][] = []; + events: Events = []; + currentSequenceId = 0; private scheduled: ReturnType | null = null; queue: SessionReplayContext[] = []; async setup(config: BrowserConfig) { - console.log('setup called', config); - this.events = [[]]; + config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); this.config = config; + this.storageKey = `${STORAGE_PREFIX}_${this.config.apiKey.substring(0, 10)}`; - config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); + await this.emptyStoreAndReset(); + + this.recordEvents(); } async execute(event: Event) { - // todo: this should be a constant/type - if (event.event_type === 'session_start') { - this.events = [[]]; - console.log('starting new session', this.events); - record({ - emit: (event) => { - const eventString = JSON.stringify(event); - const shouldSplit = shouldSplitEventsList(this.events[this.events.length - 1], eventString); - if (shouldSplit) { - this.sendEventsList(this.events[this.events.length - 1], this.events.length - 1); - this.events.push([]); - } - this.events[this.events.length - 1].push(eventString); - }, - maskAllInputs: true, - }); - } else if (event.event_type === 'session_end' && this.events.length) { - console.log('ending session', this.events); - this.sendEventsList(this.events[this.events.length - 1], this.events.length - 1); + // TODO: this should be a constant/type + if (event.event_type === 'session_end') { + if (event.session_id) { + this.sendEventsList({ + events: this.events, + sequenceId: this.currentSequenceId, + sessionId: event.session_id, + }); + } + this.events = []; + this.currentSequenceId = 0; } return event; } - sendEventsList(events: string[], index: number) { - try { - this.addToQueue({ - events, - index, - attempts: 0, - timeout: 0, - }); - } catch (e) { - this.config.loggerProvider.error(e); + async emptyStoreAndReset() { + const storedReplaySessions = await this.getAllSessionEventsFromStore(); + if (storedReplaySessions) { + for (const sessionId in storedReplaySessions) { + const storedReplayEvents = storedReplaySessions[sessionId]; + if (storedReplayEvents.events.length) { + this.sendEventsList({ + events: storedReplayEvents.events, + sequenceId: storedReplayEvents.sequenceId, + sessionId: parseInt(sessionId, 10), + }); + } + } + this.events = []; + const currentSessionStoredEvents = this.config.sessionId && storedReplaySessions[this.config.sessionId]; + this.currentSequenceId = currentSessionStoredEvents ? currentSessionStoredEvents.sequenceId + 1 : 0; + this.storeEventsForSession([], this.currentSequenceId); } } + recordEvents() { + record({ + emit: (event) => { + const eventString = JSON.stringify(event); + const shouldSplit = shouldSplitEventsList(this.events, eventString); + if (shouldSplit) { + this.sendEventsList({ + events: this.events, + sequenceId: this.currentSequenceId, + sessionId: this.config.sessionId as number, + }); + this.events = []; + this.currentSequenceId++; + } + this.events.push(eventString); + this.storeEventsForSession(this.events, this.currentSequenceId); + }, + maskAllInputs: true, + }); + } + + sendEventsList({ events, sequenceId, sessionId }: { events: string[]; sequenceId: number; sessionId: number }) { + this.addToQueue({ + events, + sequenceId, + attempts: 0, + timeout: 0, + sessionId, + }); + } + addToQueue(...list: SessionReplayContext[]) { const tryable = list.filter((context) => { if (context.attempts < this.config.flushMaxRetries) { context.attempts += 1; return true; } - throw new Error(`${MAX_RETRIES_EXCEEDED_MESSAGE}, batch index, ${context.index}`); + // TODO: should we keep events in indexdb to retry on a refresh? Whats destination behavior? + this.completeRequest({ + context, + err: `${MAX_RETRIES_EXCEEDED_MESSAGE}, batch sequence id, ${context.sequenceId}`, + }); + return false; }); - console.log('tryable items', tryable); tryable.forEach((context) => { this.queue = this.queue.concat(context); if (context.timeout === 0) { @@ -110,27 +149,19 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.scheduled = null; } - // const batches = chunk(list, this.config.flushQueueSize); // todo do we need to chunk - try { - await Promise.all(list.map((context) => this.send(context, useRetry))); - } catch (e) { - this.config.loggerProvider.error(e); - } + await Promise.all(list.map((context) => this.send(context, useRetry))); } async send(context: SessionReplayContext, useRetry = true) { - if (!this.config.apiKey) { - return Promise.reject(new Error(MISSING_API_KEY_MESSAGE)); - } const payload = { api_key: this.config.apiKey, device_id: this.config.deviceId, - session_id: this.config.sessionId, - start_timestamp: this.config.sessionId, + session_id: context.sessionId, + start_timestamp: context.sessionId, events_batch: { version: 1, events: context.events, - seq_number: context.index, + seq_number: context.sequenceId, }, }; try { @@ -143,9 +174,8 @@ export class SessionReplayPlugin implements EnrichmentPlugin { method: 'POST', }; const res = await fetch(SESSION_REPLAY_SERVER_URL, options); - console.log('res', res); if (res === null) { - return Promise.reject(new Error(UNEXPECTED_ERROR_MESSAGE)); + this.completeRequest({ context, err: UNEXPECTED_ERROR_MESSAGE, removeEvents: false }); } if (!useRetry) { let responseBody = ''; @@ -154,40 +184,98 @@ export class SessionReplayPlugin implements EnrichmentPlugin { } catch { // to avoid crash, but don't care about the error, add comment to avoid empty block lint error } - return Promise.resolve(`${res.status}: ${responseBody}`); + this.completeRequest({ context, success: `${res.status}: ${responseBody}` }); + } else { + this.handleReponse(res, context); } - return this.handleReponse(res, context); } catch (e) { - return Promise.reject(e); + this.completeRequest({ context, err: e as string, removeEvents: false }); } } - async handleReponse(res: Response, context: SessionReplayContext) { + handleReponse(res: Response, context: SessionReplayContext) { const { status } = res; const parsedStatus = new BaseTransport().buildStatus(status); switch (parsedStatus) { case Status.Success: - return this.handleSuccessResponse(res); + this.handleSuccessResponse(res, context); break; - default: - return this.handleOtherResponse(context); + this.handleOtherResponse(context); } } - async handleSuccessResponse(res: Response) { - return Promise.resolve(`${res.status}`); + handleSuccessResponse(res: Response, context: SessionReplayContext) { + this.completeRequest({ context, success: `${res.status}` }); } - async handleOtherResponse(context: SessionReplayContext) { + handleOtherResponse(context: SessionReplayContext) { + this.addToQueue({ + ...context, + timeout: context.attempts * this.retryTimeout, + }); + } + + async getAllSessionEventsFromStore() { try { - this.addToQueue({ - ...context, - timeout: context.attempts * this.retryTimeout, + const storedReplaySessionContexts: IDBStore | undefined = await get(this.storageKey); + + return storedReplaySessionContexts; + } catch (e) { + this.config.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); + } + return undefined; + } + + storeEventsForSession(events: Events, sequenceId: number) { + try { + void update(this.storageKey, (sessionMap: IDBStore | undefined): IDBStore => { + if (this.config.sessionId) { + return { + ...sessionMap, + [this.config.sessionId]: { + events: events, + sequenceId, + }, + }; + } + return sessionMap || {}; }); } catch (e) { - return Promise.reject(new Error(e as string)); + this.config.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); + } + } + + removeSessionEventsStore(sessionId: number | undefined) { + if (sessionId) { + try { + void update(this.storageKey, (sessionMap: IDBStore | undefined): IDBStore => { + sessionMap = sessionMap || {}; + delete sessionMap[sessionId]; + return sessionMap; + }); + } catch (e) { + this.config.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); + } + } + } + + completeRequest({ + context, + err, + success, + removeEvents = true, + }: { + context: SessionReplayContext; + err?: string; + success?: string; + removeEvents?: boolean; + }) { + removeEvents && this.removeSessionEventsStore(context.sessionId); + if (err) { + this.config.loggerProvider.error(err); + } else if (success) { + this.config.loggerProvider.log(success); } - return Promise.resolve(`Retrying batch at index ${context.index}`); } } diff --git a/packages/plugin-session-replay/src/typings/session-replay.ts b/packages/plugin-session-replay/src/typings/session-replay.ts index f6cb6dd17..12329dc8e 100644 --- a/packages/plugin-session-replay/src/typings/session-replay.ts +++ b/packages/plugin-session-replay/src/typings/session-replay.ts @@ -11,8 +11,15 @@ export type Events = string[]; export interface SessionReplayContext { events: Events; - index: number; + sequenceId: number; attempts: number; timeout: number; - //todo callback? + sessionId: number; +} + +export interface IDBStore { + [sessionId: number]: { + events: Events; + sequenceId: number; + }; } diff --git a/yarn.lock b/yarn.lock index 33a296cca..7770d2e1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7378,6 +7378,11 @@ iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +idb-keyval@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33" + integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg== + ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" From 95305010618222f9efb9d807c7acf2c389597d9e Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Mon, 5 Jun 2023 11:21:14 -0400 Subject: [PATCH 026/214] feat(plugins): add indentifier to session start --- packages/plugin-session-replay/src/helpers.ts | 22 +++---------------- .../src/session-replay.ts | 9 ++++++-- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/packages/plugin-session-replay/src/helpers.ts b/packages/plugin-session-replay/src/helpers.ts index 49e7ae1ae..e2c0c189b 100644 --- a/packages/plugin-session-replay/src/helpers.ts +++ b/packages/plugin-session-replay/src/helpers.ts @@ -1,6 +1,6 @@ -// const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events -// const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; -const MAX_EVENT_LIST_SIZE_IN_BYTES = 80000; +const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events +const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; +// const MAX_EVENT_LIST_SIZE_IN_BYTES = 80000; export const shouldSplitEventsList = (eventsList: string[], nextEventString: string): boolean => { const sizeOfNextEvent = new Blob([nextEventString]).size; @@ -10,19 +10,3 @@ export const shouldSplitEventsList = (eventsList: string[], nextEventString: str } return false; }; - -// Creates an array of elements split into groups the length of size. -// If array can't be split evenly, the final chunk will be the remaining elements. -// Works similary as https://lodash.com/docs/4.17.15#chunk - -export const chunk = (arr: T[], size: number) => { - const chunkSize = Math.max(size, 1); - return arr.reduce((chunks, element, index) => { - const chunkIndex = Math.floor(index / chunkSize); - if (!chunks[chunkIndex]) { - chunks[chunkIndex] = []; - } - chunks[chunkIndex].push(element); - return chunks; - }, []); -}; diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts index 8cfea7772..570453fdf 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -34,7 +34,12 @@ export class SessionReplayPlugin implements EnrichmentPlugin { async execute(event: Event) { // TODO: this should be a constant/type - if (event.event_type === 'session_end') { + if (event.event_type === 'session_start') { + event.event_properties = { + ...event.event_properties, + session_replay_enabled: true, + }; + } else if (event.event_type === 'session_end') { if (event.session_id) { this.sendEventsList({ events: this.events, @@ -45,7 +50,6 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.events = []; this.currentSequenceId = 0; } - return event; } @@ -84,6 +88,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.currentSequenceId++; } this.events.push(eventString); + console.log('this.events', this.events); this.storeEventsForSession(this.events, this.currentSequenceId); }, maskAllInputs: true, From 86ae48b0f487d81ad9ff6518a03f77fc4e142bb8 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Tue, 6 Jun 2023 10:13:25 -0400 Subject: [PATCH 027/214] feat(plugins): fix for first event fired for session replay --- .../plugin-session-replay/src/session-replay.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts index 570453fdf..0456f3b69 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -21,9 +21,14 @@ export class SessionReplayPlugin implements EnrichmentPlugin { currentSequenceId = 0; private scheduled: ReturnType | null = null; queue: SessionReplayContext[] = []; + shouldLogReplayEnabled = false; async setup(config: BrowserConfig) { config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); + if (!config.sessionId) { + this.shouldLogReplayEnabled = true; + } + this.config = config; this.storageKey = `${STORAGE_PREFIX}_${this.config.apiKey.substring(0, 10)}`; @@ -32,13 +37,14 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.recordEvents(); } - async execute(event: Event) { + execute(event: Event) { // TODO: this should be a constant/type - if (event.event_type === 'session_start') { + if (event.event_type === 'session_start' || this.shouldLogReplayEnabled) { event.event_properties = { ...event.event_properties, session_replay_enabled: true, }; + this.shouldLogReplayEnabled = false; } else if (event.event_type === 'session_end') { if (event.session_id) { this.sendEventsList({ @@ -50,7 +56,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.events = []; this.currentSequenceId = 0; } - return event; + return Promise.resolve(event); } async emptyStoreAndReset() { @@ -88,7 +94,6 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.currentSequenceId++; } this.events.push(eventString); - console.log('this.events', this.events); this.storeEventsForSession(this.events, this.currentSequenceId); }, maskAllInputs: true, From c30e3dbec8781c8906394e586c5516109f6b1192 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 8 Jun 2023 10:29:33 -0700 Subject: [PATCH 028/214] feat(plugins): restart the recording on session start --- .../src/session-replay.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts index 0456f3b69..23002a997 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -6,7 +6,7 @@ import { shouldSplitEventsList } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, UNEXPECTED_ERROR_MESSAGE } from './messages'; import { Events, IDBStore, SessionReplayContext } from './typings/session-replay'; -const SESSION_REPLAY_SERVER_URL = 'https://api2.amplitude.com/sessions/track'; +const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/track'; const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; export class SessionReplayPlugin implements EnrichmentPlugin { name = '@amplitude/plugin-session-replay'; @@ -21,13 +21,10 @@ export class SessionReplayPlugin implements EnrichmentPlugin { currentSequenceId = 0; private scheduled: ReturnType | null = null; queue: SessionReplayContext[] = []; - shouldLogReplayEnabled = false; + stopRecordingEvents: ReturnType | null = null; async setup(config: BrowserConfig) { config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); - if (!config.sessionId) { - this.shouldLogReplayEnabled = true; - } this.config = config; this.storageKey = `${STORAGE_PREFIX}_${this.config.apiKey.substring(0, 10)}`; @@ -38,13 +35,14 @@ export class SessionReplayPlugin implements EnrichmentPlugin { } execute(event: Event) { - // TODO: this should be a constant/type - if (event.event_type === 'session_start' || this.shouldLogReplayEnabled) { - event.event_properties = { - ...event.event_properties, - session_replay_enabled: true, - }; - this.shouldLogReplayEnabled = false; + event.event_properties = { + ...event.event_properties, + session_replay_enabled: true, + }; + + if (event.event_type === 'session_start' && this.stopRecordingEvents) { + this.stopRecordingEvents(); + this.recordEvents(); } else if (event.event_type === 'session_end') { if (event.session_id) { this.sendEventsList({ @@ -80,8 +78,9 @@ export class SessionReplayPlugin implements EnrichmentPlugin { } recordEvents() { - record({ + this.stopRecordingEvents = record({ emit: (event) => { + console.log('event', event); const eventString = JSON.stringify(event); const shouldSplit = shouldSplitEventsList(this.events, eventString); if (shouldSplit) { @@ -96,7 +95,6 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.events.push(eventString); this.storeEventsForSession(this.events, this.currentSequenceId); }, - maskAllInputs: true, }); } From 216f4d38d5863d133e19c9d89c0c6004e6cce294 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Wed, 21 Jun 2023 10:11:08 -0400 Subject: [PATCH 029/214] feat(plugins): session replay setting up tests --- .../src/session-replay.ts | 1 - .../test/session-replay.test.ts | 853 ++++++++++++++++++ 2 files changed, 853 insertions(+), 1 deletion(-) create mode 100644 packages/plugin-session-replay/test/session-replay.test.ts diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts index 23002a997..4891e8a06 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -80,7 +80,6 @@ export class SessionReplayPlugin implements EnrichmentPlugin { recordEvents() { this.stopRecordingEvents = record({ emit: (event) => { - console.log('event', event); const eventString = JSON.stringify(event); const shouldSplit = shouldSplitEventsList(this.events, eventString); if (shouldSplit) { diff --git a/packages/plugin-session-replay/test/session-replay.test.ts b/packages/plugin-session-replay/test/session-replay.test.ts new file mode 100644 index 000000000..e067943ba --- /dev/null +++ b/packages/plugin-session-replay/test/session-replay.test.ts @@ -0,0 +1,853 @@ +import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; +import { Logger } from '@amplitude/analytics-core'; +import { BrowserConfig, LogLevel } from '@amplitude/analytics-types'; +import { SessionReplayPlugin } from '../src/session-replay'; + +describe('SessionReplayPlugin', () => { + const mockConfig: BrowserConfig = { + apiKey: 'static_key', + flushIntervalMillis: 0, + flushMaxRetries: 0, + flushQueueSize: 0, + logLevel: LogLevel.None, + loggerProvider: new Logger(), + optOut: false, + serverUrl: 'url', + transportProvider: new FetchTransport(), + useBatch: false, + + cookieExpiration: 365, + cookieSameSite: 'Lax', + cookieSecure: false, + cookieStorage: new CookieStorage(), + cookieUpgrade: true, + disableCookies: false, + domain: '.amplitude.com', + sessionTimeout: 30 * 60 * 1000, + trackingOptions: { + ipAddress: true, + language: true, + platform: true, + }, + }; + describe('setup', () => { + test('should setup plugin', async () => { + const sessionReplay = new SessionReplayPlugin(); + await sessionReplay.setup(mockConfig); + expect(sessionReplay.config.transportProvider).toBeDefined(); + expect(sessionReplay.config.serverUrl).toBe('url'); + expect(sessionReplay.config.flushMaxRetries).toBe(0); + expect(sessionReplay.config.flushQueueSize).toBe(0); + expect(sessionReplay.config.flushIntervalMillis).toBe(0); + expect(sessionReplay.storageKey).toBe(''); + }); + + // test('should read from storage', async () => { + // const sessionReplay = new SessionReplayPlugin(); + // const config = useDefaultConfig(); + // const event = { + // event_type: 'hello', + // }; + // config.storageProvider = { + // isEnabled: async () => true, + // get: async () => undefined, + // set: async () => undefined, + // remove: async () => undefined, + // reset: async () => undefined, + // getRaw: async () => undefined, + // }; + // const get = jest.spyOn(config.storageProvider, 'get').mockResolvedValueOnce([event]); + // const execute = jest.spyOn(destination, 'execute').mockReturnValueOnce( + // Promise.resolve({ + // event, + // message: Status.Success, + // code: 200, + // }), + // ); + // await destination.setup(config); + // expect(get).toHaveBeenCalledTimes(1); + // expect(execute).toHaveBeenCalledTimes(1); + // }); + + // test('should be ok with undefined storage', async () => { + // const sessionReplay = new SessionReplayPlugin(); + // const config = useDefaultConfig(); + // config.storageProvider = undefined; + // const execute = jest.spyOn(destination, 'execute'); + // await destination.setup(config); + // expect(execute).toHaveBeenCalledTimes(0); + // }); + }); + + // describe('execute', () => { + // test('should execute plugin', async () => { + // const sessionReplay = new SessionReplayPlugin(); + // const event = { + // event_type: 'event_type', + // }; + // const addToQueue = jest.spyOn(destination, 'addToQueue').mockImplementation((context: DestinationContext) => { + // context.callback({ event, code: 200, message: Status.Success }); + // }); + // await destination.execute(event); + // expect(addToQueue).toHaveBeenCalledTimes(1); + // }); + // }); + + // describe('addToQueue', () => { + // test('should add to queue and schedule a flush', () => { + // const sessionReplay = new SessionReplayPlugin(); + // destination.config = { + // ...useDefaultConfig(), + // flushIntervalMillis: 0, + // }; + // const schedule = jest.spyOn(destination, 'schedule').mockReturnValueOnce(undefined); + // const event = { + // event_type: 'event_type', + // }; + // const context = { + // event, + // callback: () => undefined, + // attempts: 0, + // timeout: 0, + // }; + // destination.addToQueue(context); + // expect(schedule).toHaveBeenCalledTimes(1); + // expect(context.attempts).toBe(1); + // }); + // }); + + // describe('schedule', () => { + // beforeEach(() => { + // jest.useFakeTimers(); + // }); + + // afterEach(() => { + // jest.useRealTimers(); + // }); + + // test('should schedule a flush', async () => { + // const sessionReplay = new SessionReplayPlugin(); + // // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + // (destination as any).scheduled = null; + // destination.queue = [ + // { + // event: { event_type: 'event_type' }, + // attempts: 0, + // callback: () => undefined, + // timeout: 0, + // }, + // ]; + // const flush = jest + // .spyOn(destination, 'flush') + // .mockImplementationOnce(() => { + // // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + // (destination as any).scheduled = null; + // return Promise.resolve(undefined); + // }) + // .mockReturnValueOnce(Promise.resolve(undefined)); + // destination.schedule(0); + // // exhause first setTimeout + // jest.runAllTimers(); + // // wait for next tick to call nested setTimeout + // await Promise.resolve(); + // // exhause nested setTimeout + // jest.runAllTimers(); + // expect(flush).toHaveBeenCalledTimes(2); + // }); + + // test('should not schedule if one is already in progress', () => { + // const sessionReplay = new SessionReplayPlugin(); + // // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + // (destination as any).scheduled = setTimeout(jest.fn, 0); + // const flush = jest.spyOn(destination, 'flush').mockReturnValueOnce(Promise.resolve(undefined)); + // destination.schedule(0); + // expect(flush).toHaveBeenCalledTimes(0); + // }); + // }); + + // describe('flush', () => { + // test('should get batch and call send', async () => { + // const sessionReplay = new SessionReplayPlugin(); + // destination.config = { + // ...useDefaultConfig(), + // flushQueueSize: 1, + // }; + // destination.queue = [ + // { + // event: { event_type: 'event_type' }, + // attempts: 0, + // callback: () => undefined, + // timeout: 0, + // }, + // ]; + // const send = jest.spyOn(destination, 'send').mockReturnValueOnce(Promise.resolve()); + // const result = await destination.flush(); + // expect(destination.queue).toEqual([]); + // expect(result).toBe(undefined); + // expect(send).toHaveBeenCalledTimes(1); + // }); + + // test('should send with queue', async () => { + // const sessionReplay = new SessionReplayPlugin(); + // destination.config = { + // ...useDefaultConfig(), + // }; + // destination.queue = [ + // { + // event: { event_type: 'event_type' }, + // attempts: 0, + // callback: () => undefined, + // timeout: 0, + // }, + // ]; + // const send = jest.spyOn(destination, 'send').mockReturnValueOnce(Promise.resolve()); + // const result = await destination.flush(); + // expect(destination.queue).toEqual([]); + // expect(result).toBe(undefined); + // expect(send).toHaveBeenCalledTimes(1); + // }); + + // test('should send later', async () => { + // const sessionReplay = new SessionReplayPlugin(); + // destination.config = { + // ...useDefaultConfig(), + // }; + // destination.queue = [ + // { + // event: { event_type: 'event_type' }, + // attempts: 0, + // callback: () => undefined, + // timeout: 1000, + // }, + // ]; + // const send = jest.spyOn(destination, 'send').mockReturnValueOnce(Promise.resolve()); + // const result = await destination.flush(); + // expect(destination.queue).toEqual([ + // { + // event: { event_type: 'event_type' }, + // attempts: 0, + // // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + // callback: expect.any(Function), + // timeout: 1000, + // }, + // ]); + // expect(result).toBe(undefined); + // expect(send).toHaveBeenCalledTimes(0); + // }); + // }); + + // describe('send', () => { + // test('should include min id length', async () => { + // const sessionReplay = new SessionReplayPlugin(); + // const callback = jest.fn(); + // const event = { + // event_type: 'event_type', + // }; + // const context = { + // attempts: 0, + // callback, + // event, + // timeout: 0, + // }; + // const transportProvider = { + // send: jest.fn().mockImplementationOnce((_url: string, payload: Payload) => { + // expect(payload.options?.min_id_length).toBe(10); + // return Promise.resolve({ + // status: Status.Success, + // statusCode: 200, + // body: { + // eventsIngested: 1, + // payloadSizeBytes: 1, + // serverUploadTime: 1, + // }, + // }); + // }), + // }; + // await destination.setup({ + // ...useDefaultConfig(), + // transportProvider, + // apiKey: API_KEY, + // minIdLength: 10, + // }); + // await destination.send([context]); + // expect(callback).toHaveBeenCalledTimes(1); + // expect(callback).toHaveBeenCalledWith({ + // event, + // code: 200, + // message: SUCCESS_MESSAGE, + // }); + // }); + + // test('should not include extra', async () => { + // const sessionReplay = new SessionReplayPlugin(); + // const callback = jest.fn(); + // const event = { + // event_type: 'event_type', + // extra: { 'extra-key': 'extra-value' }, + // }; + // const context = { + // attempts: 0, + // callback, + // event, + // timeout: 0, + // }; + // const transportProvider = { + // send: jest.fn().mockImplementationOnce((_url: string, payload: Payload) => { + // expect(payload.options?.min_id_length).toBe(10); + // expect(payload.events.some((event) => !!event.extra)).toBeFalsy(); + // return Promise.resolve({ + // status: Status.Success, + // statusCode: 200, + // body: { + // eventsIngested: 1, + // payloadSizeBytes: 1, + // serverUploadTime: 1, + // }, + // }); + // }), + // }; + // await destination.setup({ + // ...useDefaultConfig(), + // transportProvider, + // apiKey: API_KEY, + // minIdLength: 10, + // }); + // await destination.send([context]); + // expect(callback).toHaveBeenCalledTimes(1); + // expect(callback).toHaveBeenCalledWith({ + // event, + // code: 200, + // message: SUCCESS_MESSAGE, + // }); + // }); + + // test('should not retry', async () => { + // const sessionReplay = new SessionReplayPlugin(); + // const callback = jest.fn(); + // const event = { + // event_type: 'event_type', + // }; + // const context = { + // attempts: 0, + // callback, + // event, + // timeout: 0, + // }; + // const transportProvider = { + // send: jest.fn().mockImplementationOnce(() => { + // return Promise.resolve({ + // status: Status.Failed, + // statusCode: 500, + // }); + // }), + // }; + // await destination.setup({ + // ...useDefaultConfig(), + // transportProvider, + // apiKey: API_KEY, + // }); + // await destination.send([context], false); + // expect(callback).toHaveBeenCalledTimes(1); + // expect(callback).toHaveBeenCalledWith({ + // event, + // code: 500, + // message: Status.Failed, + // }); + // }); + + // test('should provide error details', async () => { + // const sessionReplay = new SessionReplayPlugin(); + // const callback = jest.fn(); + // const event = { + // event_type: 'event_type', + // }; + // const context = { + // attempts: 0, + // callback, + // event, + // timeout: 0, + // }; + // const body = { + // error: 'Request missing required field', + // missingField: 'user_id', + // }; + // const transportProvider = { + // send: jest.fn().mockImplementationOnce(() => { + // return Promise.resolve({ + // status: Status.Invalid, + // statusCode: 400, + // body, + // }); + // }), + // }; + // await destination.setup({ + // ...useDefaultConfig(), + // transportProvider, + // apiKey: API_KEY, + // }); + // await destination.send([context], false); + // expect(callback).toHaveBeenCalledTimes(1); + // expect(callback).toHaveBeenCalledWith({ + // event, + // code: 400, + // message: `${Status.Invalid}: ${JSON.stringify(body, null, 2)}`, + // }); + // }); + + // test('should handle no api key', async () => { + // const sessionReplay = new SessionReplayPlugin(); + // const callback = jest.fn(); + // const event = { + // event_type: 'event_type', + // }; + // const context = { + // attempts: 0, + // callback, + // event, + // timeout: 0, + // }; + // const transportProvider = { + // send: jest.fn().mockImplementationOnce(() => { + // throw new Error(); + // }), + // }; + // await destination.setup({ + // ...useDefaultConfig(), + // transportProvider, + // apiKey: '', + // }); + // await destination.send([context]); + // expect(callback).toHaveBeenCalledTimes(1); + // expect(callback).toHaveBeenCalledWith({ + // event, + // code: 400, + // message: MISSING_API_KEY_MESSAGE, + // }); + // }); + + // test('should handle unexpected error', async () => { + // const sessionReplay = new SessionReplayPlugin(); + // const callback = jest.fn(); + // const context = { + // attempts: 0, + // callback, + // event: { + // event_type: 'event_type', + // }, + // timeout: 0, + // }; + // const transportProvider = { + // send: jest.fn().mockImplementationOnce(() => { + // throw new Error(); + // }), + // }; + // await destination.setup({ + // ...useDefaultConfig(), + // transportProvider, + // }); + // await destination.send([context]); + // expect(callback).toHaveBeenCalledTimes(1); + // }); + // }); + + // describe('saveEvents', () => { + // test('should save to storage provider', () => { + // const sessionReplay = new SessionReplayPlugin(); + // destination.config = useDefaultConfig(); + // destination.config.storageProvider = { + // isEnabled: async () => true, + // get: async () => undefined, + // set: async () => undefined, + // remove: async () => undefined, + // reset: async () => undefined, + // getRaw: async () => undefined, + // }; + // const set = jest.spyOn(destination.config.storageProvider, 'set').mockResolvedValueOnce(undefined); + // destination.saveEvents(); + // expect(set).toHaveBeenCalledTimes(1); + // }); + + // test('should be ok with no storage provider', () => { + // const sessionReplay = new SessionReplayPlugin(); + // destination.config = useDefaultConfig(); + // destination.config.storageProvider = undefined; + // expect(destination.saveEvents()).toBe(undefined); + // }); + // }); + + // describe('module level integration', () => { + // const successResponse = { + // status: Status.Success, + // statusCode: 200, + // body: { + // eventsIngested: 1, + // payloadSizeBytes: 1, + // serverUploadTime: 1, + // }, + // }; + + // test('should handle unexpected error', async () => { + // class Http { + // send = jest.fn().mockImplementationOnce(() => { + // return Promise.resolve(null); + // }); + // } + // const transportProvider = new Http(); + // const sessionReplay = new SessionReplayPlugin(); + // const config = { + // ...useDefaultConfig(), + // flushQueueSize: 2, + // flushIntervalMillis: 500, + // transportProvider, + // }; + // await destination.setup(config); + // const result = await destination.execute({ + // event_type: 'event_type', + // }); + // expect(result.code).toBe(0); + // expect(result.message).toBe(UNEXPECTED_ERROR_MESSAGE); + // expect(transportProvider.send).toHaveBeenCalledTimes(1); + // }); + + // test('should not retry with invalid api key', async () => { + // class Http { + // send = jest.fn().mockImplementationOnce(() => { + // return Promise.resolve({ + // status: Status.Invalid, + // statusCode: 400, + // body: { + // error: INVALID_API_KEY, + // }, + // }); + // }); + // } + // const transportProvider = new Http(); + // const sessionReplay = new SessionReplayPlugin(); + // destination.retryTimeout = 10; + // const config = { + // ...useDefaultConfig(), + // flushQueueSize: 2, + // flushIntervalMillis: 500, + // transportProvider, + // }; + // await destination.setup(config); + // const results = await Promise.all([ + // destination.execute({ + // event_type: 'event_type', + // }), + // destination.execute({ + // event_type: 'event_type', + // }), + // ]); + // expect(results[0].code).toBe(400); + // expect(transportProvider.send).toHaveBeenCalledTimes(1); + // }); + + // test('should handle retry for 400 error', async () => { + // class Http { + // send = jest + // .fn() + // .mockImplementationOnce(() => { + // return Promise.resolve({ + // status: Status.Invalid, + // statusCode: 400, + // body: { + // error: 'error', + // missingField: '', + // eventsWithInvalidFields: { a: [0] }, + // eventsWithMissingFields: { b: [] }, + // eventsWithInvalidIdLengths: {}, + // silencedEvents: [], + // }, + // }); + // }) + // .mockImplementationOnce(() => { + // return Promise.resolve(successResponse); + // }); + // } + // const transportProvider = new Http(); + // const sessionReplay = new SessionReplayPlugin(); + // destination.retryTimeout = 10; + // const config = { + // ...useDefaultConfig(), + // flushQueueSize: 2, + // flushIntervalMillis: 500, + // transportProvider, + // }; + // await destination.setup(config); + // const results = await Promise.all([ + // destination.execute({ + // event_type: 'event_type', + // }), + // destination.execute({ + // event_type: 'event_type', + // }), + // ]); + // expect(results[0].code).toBe(400); + // expect(results[1].code).toBe(200); + // expect(transportProvider.send).toHaveBeenCalledTimes(2); + // }); + + // test('should handle retry for 400 error with missing body field', async () => { + // class Http { + // send = jest.fn().mockImplementationOnce(() => { + // return Promise.resolve({ + // status: Status.Invalid, + // statusCode: 400, + // body: { + // error: 'error', + // missingField: 'key', + // eventsWithInvalidFields: {}, + // eventsWithMissingFields: {}, + // silencedEvents: [], + // }, + // }); + // }); + // } + // const transportProvider = new Http(); + // const sessionReplay = new SessionReplayPlugin(); + // const config = { + // ...useDefaultConfig(), + // flushQueueSize: 2, + // flushIntervalMillis: 500, + // transportProvider, + // }; + // await destination.setup(config); + // const results = await Promise.all([ + // destination.execute({ + // event_type: 'event_type', + // }), + // destination.execute({ + // event_type: 'event_type', + // }), + // ]); + // expect(results[0].code).toBe(400); + // expect(results[1].code).toBe(400); + // expect(transportProvider.send).toHaveBeenCalledTimes(1); + // }); + + // test('should handle retry for 413 error with flushQueueSize of 1', async () => { + // class Http { + // send = jest.fn().mockImplementationOnce(() => { + // return Promise.resolve({ + // status: Status.PayloadTooLarge, + // statusCode: 413, + // body: { + // error: 'error', + // }, + // }); + // }); + // } + // const transportProvider = new Http(); + // const sessionReplay = new SessionReplayPlugin(); + // const config = { + // ...useDefaultConfig(), + // flushQueueSize: 1, + // flushIntervalMillis: 500, + // transportProvider, + // }; + // await destination.setup(config); + // const event = { + // event_type: 'event_type', + // }; + // const result = await destination.execute(event); + // expect(result).toEqual({ + // event, + // message: 'error', + // code: 413, + // }); + // expect(transportProvider.send).toHaveBeenCalledTimes(1); + // }); + + // test('should handle retry for 413 error', async () => { + // class Http { + // send = jest + // .fn() + // .mockImplementationOnce(() => { + // return Promise.resolve({ + // status: Status.PayloadTooLarge, + // statusCode: 413, + // body: { + // error: 'error', + // }, + // }); + // }) + // .mockImplementationOnce(() => { + // return Promise.resolve(successResponse); + // }) + // .mockImplementationOnce(() => { + // return Promise.resolve(successResponse); + // }); + // } + // const transportProvider = new Http(); + // const sessionReplay = new SessionReplayPlugin(); + // destination.retryTimeout = 10; + // const config = { + // ...useDefaultConfig(), + // flushQueueSize: 2, + // flushIntervalMillis: 500, + // transportProvider, + // }; + // await destination.setup(config); + // await Promise.all([ + // destination.execute({ + // event_type: 'event_type', + // }), + // destination.execute({ + // event_type: 'event_type', + // }), + // ]); + // expect(transportProvider.send).toHaveBeenCalledTimes(3); + // }); + + // test('should handle retry for 429 error', async () => { + // class Http { + // send = jest + // .fn() + // .mockImplementationOnce(() => { + // return Promise.resolve({ + // status: Status.RateLimit, + // statusCode: 429, + // body: { + // error: 'error', + // epsThreshold: 1, + // throttledDevices: {}, + // throttledUsers: {}, + // exceededDailyQuotaDevices: { + // '1': 1, + // }, + // exceededDailyQuotaUsers: { + // '2': 1, + // }, + // throttledEvents: [0], + // }, + // }); + // }) + // .mockImplementationOnce(() => { + // return Promise.resolve(successResponse); + // }) + // .mockImplementationOnce(() => { + // return Promise.resolve(successResponse); + // }); + // } + // const transportProvider = new Http(); + // const sessionReplay = new SessionReplayPlugin(); + // destination.retryTimeout = 10; + // destination.throttleTimeout = 1; + // const config = { + // ...useDefaultConfig(), + // flushQueueSize: 4, + // flushIntervalMillis: 500, + // transportProvider, + // }; + // await destination.setup(config); + // const results = await Promise.all([ + // // throttled + // destination.execute({ + // event_type: 'event_type', + // user_id: '0', + // device_id: '0', + // }), + // // exceed daily device quota + // destination.execute({ + // event_type: 'event_type', + // user_id: '1', + // device_id: '1', + // }), + // // exceed daily user quota + // destination.execute({ + // event_type: 'event_type', + // user_id: '2', + // device_id: '2', + // }), + // // success + // destination.execute({ + // event_type: 'event_type', + // user_id: '3', + // device_id: '3', + // }), + // ]); + // expect(results[0].code).toBe(200); + // expect(results[1].code).toBe(429); + // expect(results[2].code).toBe(429); + // expect(results[3].code).toBe(200); + // expect(transportProvider.send).toHaveBeenCalledTimes(2); + // }); + + // test('should handle retry for 500 error', async () => { + // class Http { + // send = jest + // .fn() + // .mockImplementationOnce(() => { + // return Promise.resolve({ + // statusCode: 500, + // status: Status.Failed, + // }); + // }) + // .mockImplementationOnce(() => { + // return Promise.resolve(successResponse); + // }); + // } + // const transportProvider = new Http(); + // const sessionReplay = new SessionReplayPlugin(); + // destination.retryTimeout = 10; + // const config = { + // ...useDefaultConfig(), + // flushQueueSize: 2, + // flushIntervalMillis: 500, + // transportProvider, + // }; + // await destination.setup(config); + // await Promise.all([ + // destination.execute({ + // event_type: 'event_type', + // }), + // destination.execute({ + // event_type: 'event_type', + // }), + // ]); + // expect(transportProvider.send).toHaveBeenCalledTimes(2); + // }); + + // test('should handle retry for 503 error', async () => { + // class Http { + // send = jest + // .fn() + // .mockImplementationOnce(() => { + // return Promise.resolve({ + // statusCode: 500, + // status: Status.Failed, + // }); + // }) + // .mockImplementationOnce(() => { + // return Promise.resolve({ + // statusCode: 500, + // status: Status.Failed, + // }); + // }); + // } + // const transportProvider = new Http(); + // const sessionReplay = new SessionReplayPlugin(); + // destination.retryTimeout = 10; + // const config = { + // ...useDefaultConfig(), + // flushMaxRetries: 1, + // flushQueueSize: 2, + // flushIntervalMillis: 500, + // transportProvider, + // }; + // await destination.setup(config); + // const results = await Promise.all([ + // destination.execute({ + // event_type: 'event_type', + // }), + // destination.execute({ + // event_type: 'event_type', + // }), + // ]); + // expect(results[0].code).toBe(500); + // expect(results[1].code).toBe(500); + // expect(transportProvider.send).toHaveBeenCalledTimes(1); + // }); + // }); +}); From 6211a3e33c215e429a005128e50323203369fc7a Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 22 Jun 2023 12:51:44 -0400 Subject: [PATCH 030/214] feat(plugins): add test coverage to session replay plugin --- .../src/session-replay.ts | 21 +- .../test/session-replay.test.ts | 1302 +++++++---------- 2 files changed, 522 insertions(+), 801 deletions(-) diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts index 4891e8a06..0dd9db568 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -1,6 +1,6 @@ import { AMPLITUDE_PREFIX, BaseTransport } from '@amplitude/analytics-core'; import { BrowserConfig, EnrichmentPlugin, Event, PluginType, Status } from '@amplitude/analytics-types'; -import { get, update } from 'idb-keyval'; +import * as IDBKeyVal from 'idb-keyval'; import { record } from 'rrweb'; import { shouldSplitEventsList } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, UNEXPECTED_ERROR_MESSAGE } from './messages'; @@ -28,7 +28,6 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.config = config; this.storageKey = `${STORAGE_PREFIX}_${this.config.apiKey.substring(0, 10)}`; - await this.emptyStoreAndReset(); this.recordEvents(); @@ -39,7 +38,6 @@ export class SessionReplayPlugin implements EnrichmentPlugin { ...event.event_properties, session_replay_enabled: true, }; - if (event.event_type === 'session_start' && this.stopRecordingEvents) { this.stopRecordingEvents(); this.recordEvents(); @@ -193,27 +191,26 @@ export class SessionReplayPlugin implements EnrichmentPlugin { } this.completeRequest({ context, success: `${res.status}: ${responseBody}` }); } else { - this.handleReponse(res, context); + this.handleReponse(res.status, context); } } catch (e) { this.completeRequest({ context, err: e as string, removeEvents: false }); } } - handleReponse(res: Response, context: SessionReplayContext) { - const { status } = res; + handleReponse(status: number, context: SessionReplayContext) { const parsedStatus = new BaseTransport().buildStatus(status); switch (parsedStatus) { case Status.Success: - this.handleSuccessResponse(res, context); + this.handleSuccessResponse(context); break; default: this.handleOtherResponse(context); } } - handleSuccessResponse(res: Response, context: SessionReplayContext) { - this.completeRequest({ context, success: `${res.status}` }); + handleSuccessResponse(context: SessionReplayContext) { + this.completeRequest({ context, success: `Session Replay events tracked successfully` }); } handleOtherResponse(context: SessionReplayContext) { @@ -225,7 +222,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { async getAllSessionEventsFromStore() { try { - const storedReplaySessionContexts: IDBStore | undefined = await get(this.storageKey); + const storedReplaySessionContexts: IDBStore | undefined = await IDBKeyVal.get(this.storageKey); return storedReplaySessionContexts; } catch (e) { @@ -236,7 +233,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { storeEventsForSession(events: Events, sequenceId: number) { try { - void update(this.storageKey, (sessionMap: IDBStore | undefined): IDBStore => { + void IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore | undefined): IDBStore => { if (this.config.sessionId) { return { ...sessionMap, @@ -256,7 +253,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { removeSessionEventsStore(sessionId: number | undefined) { if (sessionId) { try { - void update(this.storageKey, (sessionMap: IDBStore | undefined): IDBStore => { + void IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore | undefined): IDBStore => { sessionMap = sessionMap || {}; delete sessionMap[sessionId]; return sessionMap; diff --git a/packages/plugin-session-replay/test/session-replay.test.ts b/packages/plugin-session-replay/test/session-replay.test.ts index e067943ba..00e9fb3d1 100644 --- a/packages/plugin-session-replay/test/session-replay.test.ts +++ b/packages/plugin-session-replay/test/session-replay.test.ts @@ -1,16 +1,46 @@ +/* eslint-disable jest/expect-expect */ import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; -import { Logger } from '@amplitude/analytics-core'; import { BrowserConfig, LogLevel } from '@amplitude/analytics-types'; +import * as IDBKeyVal from 'idb-keyval'; +import * as RRWeb from 'rrweb'; import { SessionReplayPlugin } from '../src/session-replay'; +jest.mock('idb-keyval'); +type MockedIDBKeyVal = jest.Mocked; + +jest.mock('rrweb'); +type MockedRRWeb = jest.Mocked; + +const mockEvent = + '{"type":4,"data":{"href":"https://analytics.amplitude.com/","width":1728,"height":154},"timestamp":1687358660935}'; + +async function runScheduleTimers() { + // exhause first setTimeout + jest.runAllTimers(); + // wait for next tick to call nested setTimeout + await Promise.resolve(); + // exhause nested setTimeout + jest.runAllTimers(); +} + describe('SessionReplayPlugin', () => { + const { get, update } = IDBKeyVal as MockedIDBKeyVal; + const { record } = RRWeb as MockedRRWeb; + let originalFetch: typeof global.fetch; const mockConfig: BrowserConfig = { apiKey: 'static_key', flushIntervalMillis: 0, - flushMaxRetries: 0, + flushMaxRetries: 1, flushQueueSize: 0, logLevel: LogLevel.None, - loggerProvider: new Logger(), + loggerProvider: { + error: jest.fn(), + log: jest.fn(), + disable: jest.fn(), + enable: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }, optOut: false, serverUrl: 'url', transportProvider: new FetchTransport(), @@ -30,824 +60,518 @@ describe('SessionReplayPlugin', () => { platform: true, }, }; + beforeAll(() => { + jest.useFakeTimers(); + }); + beforeEach(() => { + originalFetch = global.fetch; + global.fetch = jest.fn(() => + Promise.resolve({ + status: 200, + }), + ) as jest.Mock; + }); + afterEach(() => { + jest.clearAllMocks(); + global.fetch = originalFetch; + }); + afterAll(() => { + jest.useRealTimers(); + }); describe('setup', () => { test('should setup plugin', async () => { const sessionReplay = new SessionReplayPlugin(); await sessionReplay.setup(mockConfig); expect(sessionReplay.config.transportProvider).toBeDefined(); expect(sessionReplay.config.serverUrl).toBe('url'); - expect(sessionReplay.config.flushMaxRetries).toBe(0); + expect(sessionReplay.config.flushMaxRetries).toBe(1); expect(sessionReplay.config.flushQueueSize).toBe(0); expect(sessionReplay.config.flushIntervalMillis).toBe(0); - expect(sessionReplay.storageKey).toBe(''); + expect(sessionReplay.storageKey).toBe('AMP_replay_unsent_static_key'); }); - // test('should read from storage', async () => { - // const sessionReplay = new SessionReplayPlugin(); - // const config = useDefaultConfig(); - // const event = { - // event_type: 'hello', - // }; - // config.storageProvider = { - // isEnabled: async () => true, - // get: async () => undefined, - // set: async () => undefined, - // remove: async () => undefined, - // reset: async () => undefined, - // getRaw: async () => undefined, - // }; - // const get = jest.spyOn(config.storageProvider, 'get').mockResolvedValueOnce([event]); - // const execute = jest.spyOn(destination, 'execute').mockReturnValueOnce( - // Promise.resolve({ - // event, - // message: Status.Success, - // code: 200, - // }), - // ); - // await destination.setup(config); - // expect(get).toHaveBeenCalledTimes(1); - // expect(execute).toHaveBeenCalledTimes(1); - // }); - - // test('should be ok with undefined storage', async () => { - // const sessionReplay = new SessionReplayPlugin(); - // const config = useDefaultConfig(); - // config.storageProvider = undefined; - // const execute = jest.spyOn(destination, 'execute'); - // await destination.setup(config); - // expect(execute).toHaveBeenCalledTimes(0); - // }); - }); - - // describe('execute', () => { - // test('should execute plugin', async () => { - // const sessionReplay = new SessionReplayPlugin(); - // const event = { - // event_type: 'event_type', - // }; - // const addToQueue = jest.spyOn(destination, 'addToQueue').mockImplementation((context: DestinationContext) => { - // context.callback({ event, code: 200, message: Status.Success }); - // }); - // await destination.execute(event); - // expect(addToQueue).toHaveBeenCalledTimes(1); - // }); - // }); - - // describe('addToQueue', () => { - // test('should add to queue and schedule a flush', () => { - // const sessionReplay = new SessionReplayPlugin(); - // destination.config = { - // ...useDefaultConfig(), - // flushIntervalMillis: 0, - // }; - // const schedule = jest.spyOn(destination, 'schedule').mockReturnValueOnce(undefined); - // const event = { - // event_type: 'event_type', - // }; - // const context = { - // event, - // callback: () => undefined, - // attempts: 0, - // timeout: 0, - // }; - // destination.addToQueue(context); - // expect(schedule).toHaveBeenCalledTimes(1); - // expect(context.attempts).toBe(1); - // }); - // }); - - // describe('schedule', () => { - // beforeEach(() => { - // jest.useFakeTimers(); - // }); + // polluting fetch + test('should read events from storage and send them, then reset storage for session', async () => { + const sessionReplay = new SessionReplayPlugin(); + mockConfig.sessionId = 456; + get.mockResolvedValueOnce({ + 123: { + events: [mockEvent], + sequenceId: 3, + }, + 456: { + events: [mockEvent], + sequenceId: 1, + }, + }); + const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); - // afterEach(() => { - // jest.useRealTimers(); - // }); + await sessionReplay.setup(mockConfig); + jest.runAllTimers(); + expect(send).toHaveBeenCalledTimes(2); - // test('should schedule a flush', async () => { - // const sessionReplay = new SessionReplayPlugin(); - // // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - // (destination as any).scheduled = null; - // destination.queue = [ - // { - // event: { event_type: 'event_type' }, - // attempts: 0, - // callback: () => undefined, - // timeout: 0, - // }, - // ]; - // const flush = jest - // .spyOn(destination, 'flush') - // .mockImplementationOnce(() => { - // // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - // (destination as any).scheduled = null; - // return Promise.resolve(undefined); - // }) - // .mockReturnValueOnce(Promise.resolve(undefined)); - // destination.schedule(0); - // // exhause first setTimeout - // jest.runAllTimers(); - // // wait for next tick to call nested setTimeout - // await Promise.resolve(); - // // exhause nested setTimeout - // jest.runAllTimers(); - // expect(flush).toHaveBeenCalledTimes(2); - // }); + // Sending first stored session events + expect(send.mock.calls[0][0]).toEqual({ + attempts: 1, + events: [mockEvent], + sequenceId: 3, + sessionId: 123, + timeout: 0, + }); + // Sending second stored session events + expect(send.mock.calls[1][0]).toEqual({ + attempts: 1, + events: [mockEvent], + sequenceId: 1, + sessionId: 456, + timeout: 0, + }); - // test('should not schedule if one is already in progress', () => { - // const sessionReplay = new SessionReplayPlugin(); - // // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - // (destination as any).scheduled = setTimeout(jest.fn, 0); - // const flush = jest.spyOn(destination, 'flush').mockReturnValueOnce(Promise.resolve(undefined)); - // destination.schedule(0); - // expect(flush).toHaveBeenCalledTimes(0); - // }); - // }); + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][1]({})).toEqual({ + '456': { + events: [], + sequenceId: 2, + }, + }); + }); + test('should record events', async () => { + const sessionReplay = new SessionReplayPlugin(); + await sessionReplay.setup(mockConfig); + jest.runAllTimers(); + expect(record).toHaveBeenCalledTimes(1); + }); + }); - // describe('flush', () => { - // test('should get batch and call send', async () => { - // const sessionReplay = new SessionReplayPlugin(); - // destination.config = { - // ...useDefaultConfig(), - // flushQueueSize: 1, - // }; - // destination.queue = [ - // { - // event: { event_type: 'event_type' }, - // attempts: 0, - // callback: () => undefined, - // timeout: 0, - // }, - // ]; - // const send = jest.spyOn(destination, 'send').mockReturnValueOnce(Promise.resolve()); - // const result = await destination.flush(); - // expect(destination.queue).toEqual([]); - // expect(result).toBe(undefined); - // expect(send).toHaveBeenCalledTimes(1); - // }); + describe('execute', () => { + test('should add event property for session_replay_enabled', async () => { + const sessionReplay = new SessionReplayPlugin(); + await sessionReplay.setup(mockConfig); + const event = { + event_type: 'event_type', + event_properties: { + property_a: true, + property_b: 123, + }, + }; - // test('should send with queue', async () => { - // const sessionReplay = new SessionReplayPlugin(); - // destination.config = { - // ...useDefaultConfig(), - // }; - // destination.queue = [ - // { - // event: { event_type: 'event_type' }, - // attempts: 0, - // callback: () => undefined, - // timeout: 0, - // }, - // ]; - // const send = jest.spyOn(destination, 'send').mockReturnValueOnce(Promise.resolve()); - // const result = await destination.flush(); - // expect(destination.queue).toEqual([]); - // expect(result).toBe(undefined); - // expect(send).toHaveBeenCalledTimes(1); - // }); + const enrichedEvent = await sessionReplay.execute(event); + expect(enrichedEvent.event_properties).toEqual({ + property_a: true, + property_b: 123, + session_replay_enabled: true, + }); + }); - // test('should send later', async () => { - // const sessionReplay = new SessionReplayPlugin(); - // destination.config = { - // ...useDefaultConfig(), - // }; - // destination.queue = [ - // { - // event: { event_type: 'event_type' }, - // attempts: 0, - // callback: () => undefined, - // timeout: 1000, - // }, - // ]; - // const send = jest.spyOn(destination, 'send').mockReturnValueOnce(Promise.resolve()); - // const result = await destination.flush(); - // expect(destination.queue).toEqual([ - // { - // event: { event_type: 'event_type' }, - // attempts: 0, - // // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - // callback: expect.any(Function), - // timeout: 1000, - // }, - // ]); - // expect(result).toBe(undefined); - // expect(send).toHaveBeenCalledTimes(0); - // }); - // }); + test('should restart recording events when session_start fires', async () => { + const sessionReplay = new SessionReplayPlugin(); + const mockStopRecordingEvents = jest.fn(); + record.mockReturnValue(mockStopRecordingEvents); + await sessionReplay.setup(mockConfig); + jest.runAllTimers(); + const event = { + event_type: 'session_start', + }; + await sessionReplay.execute(event); + expect(mockStopRecordingEvents).toHaveBeenCalledTimes(1); + expect(record).toHaveBeenCalledTimes(2); + }); - // describe('send', () => { - // test('should include min id length', async () => { - // const sessionReplay = new SessionReplayPlugin(); - // const callback = jest.fn(); - // const event = { - // event_type: 'event_type', - // }; - // const context = { - // attempts: 0, - // callback, - // event, - // timeout: 0, - // }; - // const transportProvider = { - // send: jest.fn().mockImplementationOnce((_url: string, payload: Payload) => { - // expect(payload.options?.min_id_length).toBe(10); - // return Promise.resolve({ - // status: Status.Success, - // statusCode: 200, - // body: { - // eventsIngested: 1, - // payloadSizeBytes: 1, - // serverUploadTime: 1, - // }, - // }); - // }), - // }; - // await destination.setup({ - // ...useDefaultConfig(), - // transportProvider, - // apiKey: API_KEY, - // minIdLength: 10, - // }); - // await destination.send([context]); - // expect(callback).toHaveBeenCalledTimes(1); - // expect(callback).toHaveBeenCalledWith({ - // event, - // code: 200, - // message: SUCCESS_MESSAGE, - // }); - // }); + test('should send the current events list when session_end fires', async () => { + const sessionReplay = new SessionReplayPlugin(); + sessionReplay.config = mockConfig; + const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); - // test('should not include extra', async () => { - // const sessionReplay = new SessionReplayPlugin(); - // const callback = jest.fn(); - // const event = { - // event_type: 'event_type', - // extra: { 'extra-key': 'extra-value' }, - // }; - // const context = { - // attempts: 0, - // callback, - // event, - // timeout: 0, - // }; - // const transportProvider = { - // send: jest.fn().mockImplementationOnce((_url: string, payload: Payload) => { - // expect(payload.options?.min_id_length).toBe(10); - // expect(payload.events.some((event) => !!event.extra)).toBeFalsy(); - // return Promise.resolve({ - // status: Status.Success, - // statusCode: 200, - // body: { - // eventsIngested: 1, - // payloadSizeBytes: 1, - // serverUploadTime: 1, - // }, - // }); - // }), - // }; - // await destination.setup({ - // ...useDefaultConfig(), - // transportProvider, - // apiKey: API_KEY, - // minIdLength: 10, - // }); - // await destination.send([context]); - // expect(callback).toHaveBeenCalledTimes(1); - // expect(callback).toHaveBeenCalledWith({ - // event, - // code: 200, - // message: SUCCESS_MESSAGE, - // }); - // }); + const event = { + event_type: 'session_end', + session_id: 789, + }; + sessionReplay.events = [mockEvent]; + await sessionReplay.execute(event); + jest.runAllTimers(); + expect(send).toHaveBeenCalledTimes(1); - // test('should not retry', async () => { - // const sessionReplay = new SessionReplayPlugin(); - // const callback = jest.fn(); - // const event = { - // event_type: 'event_type', - // }; - // const context = { - // attempts: 0, - // callback, - // event, - // timeout: 0, - // }; - // const transportProvider = { - // send: jest.fn().mockImplementationOnce(() => { - // return Promise.resolve({ - // status: Status.Failed, - // statusCode: 500, - // }); - // }), - // }; - // await destination.setup({ - // ...useDefaultConfig(), - // transportProvider, - // apiKey: API_KEY, - // }); - // await destination.send([context], false); - // expect(callback).toHaveBeenCalledTimes(1); - // expect(callback).toHaveBeenCalledWith({ - // event, - // code: 500, - // message: Status.Failed, - // }); - // }); + // Sending first stored session events + expect(send.mock.calls[0][0]).toEqual({ + attempts: 1, + events: [mockEvent], + sequenceId: 0, + sessionId: 789, + timeout: 0, + }); + }); + }); - // test('should provide error details', async () => { - // const sessionReplay = new SessionReplayPlugin(); - // const callback = jest.fn(); - // const event = { - // event_type: 'event_type', - // }; - // const context = { - // attempts: 0, - // callback, - // event, - // timeout: 0, - // }; - // const body = { - // error: 'Request missing required field', - // missingField: 'user_id', - // }; - // const transportProvider = { - // send: jest.fn().mockImplementationOnce(() => { - // return Promise.resolve({ - // status: Status.Invalid, - // statusCode: 400, - // body, - // }); - // }), - // }; - // await destination.setup({ - // ...useDefaultConfig(), - // transportProvider, - // apiKey: API_KEY, - // }); - // await destination.send([context], false); - // expect(callback).toHaveBeenCalledTimes(1); - // expect(callback).toHaveBeenCalledWith({ - // event, - // code: 400, - // message: `${Status.Invalid}: ${JSON.stringify(body, null, 2)}`, - // }); - // }); + describe('addToQueue', () => { + test('should add to queue and schedule a flush', () => { + const sessionReplay = new SessionReplayPlugin(); + sessionReplay.config = mockConfig; + const schedule = jest.spyOn(sessionReplay, 'schedule').mockReturnValueOnce(undefined); + const context = { + events: [mockEvent], + sequenceId: 1, + sessionId: 123, + attempts: 0, + timeout: 0, + }; + sessionReplay.addToQueue(context); + expect(schedule).toHaveBeenCalledTimes(1); + expect(context.attempts).toBe(1); + }); + }); - // test('should handle no api key', async () => { - // const sessionReplay = new SessionReplayPlugin(); - // const callback = jest.fn(); - // const event = { - // event_type: 'event_type', - // }; - // const context = { - // attempts: 0, - // callback, - // event, - // timeout: 0, - // }; - // const transportProvider = { - // send: jest.fn().mockImplementationOnce(() => { - // throw new Error(); - // }), - // }; - // await destination.setup({ - // ...useDefaultConfig(), - // transportProvider, - // apiKey: '', - // }); - // await destination.send([context]); - // expect(callback).toHaveBeenCalledTimes(1); - // expect(callback).toHaveBeenCalledWith({ - // event, - // code: 400, - // message: MISSING_API_KEY_MESSAGE, - // }); - // }); + describe('schedule', () => { + test('should schedule a flush', async () => { + const sessionReplay = new SessionReplayPlugin(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (sessionReplay as any).scheduled = null; + sessionReplay.queue = [ + { + events: [mockEvent], + sequenceId: 1, + sessionId: 123, + attempts: 0, + timeout: 0, + }, + ]; + const flush = jest + .spyOn(sessionReplay, 'flush') + .mockImplementationOnce(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (sessionReplay as any).scheduled = null; + return Promise.resolve(undefined); + }) + .mockReturnValueOnce(Promise.resolve(undefined)); + sessionReplay.schedule(0); + await runScheduleTimers(); + expect(flush).toHaveBeenCalledTimes(2); + }); - // test('should handle unexpected error', async () => { - // const sessionReplay = new SessionReplayPlugin(); - // const callback = jest.fn(); - // const context = { - // attempts: 0, - // callback, - // event: { - // event_type: 'event_type', - // }, - // timeout: 0, - // }; - // const transportProvider = { - // send: jest.fn().mockImplementationOnce(() => { - // throw new Error(); - // }), - // }; - // await destination.setup({ - // ...useDefaultConfig(), - // transportProvider, - // }); - // await destination.send([context]); - // expect(callback).toHaveBeenCalledTimes(1); - // }); - // }); + test('should not schedule if one is already in progress', () => { + const sessionReplay = new SessionReplayPlugin(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (sessionReplay as any).scheduled = setTimeout(jest.fn, 0); + const flush = jest.spyOn(sessionReplay, 'flush').mockReturnValueOnce(Promise.resolve(undefined)); + sessionReplay.schedule(0); + expect(flush).toHaveBeenCalledTimes(0); + }); + }); - // describe('saveEvents', () => { - // test('should save to storage provider', () => { - // const sessionReplay = new SessionReplayPlugin(); - // destination.config = useDefaultConfig(); - // destination.config.storageProvider = { - // isEnabled: async () => true, - // get: async () => undefined, - // set: async () => undefined, - // remove: async () => undefined, - // reset: async () => undefined, - // getRaw: async () => undefined, - // }; - // const set = jest.spyOn(destination.config.storageProvider, 'set').mockResolvedValueOnce(undefined); - // destination.saveEvents(); - // expect(set).toHaveBeenCalledTimes(1); - // }); + describe('flush', () => { + test('should call send', async () => { + const sessionReplay = new SessionReplayPlugin(); + sessionReplay.config = mockConfig; + sessionReplay.queue = [ + { + events: [mockEvent], + sequenceId: 1, + sessionId: 123, + attempts: 0, + timeout: 0, + }, + ]; + const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); + const result = await sessionReplay.flush(); + expect(sessionReplay.queue).toEqual([]); + expect(result).toBe(undefined); + expect(send).toHaveBeenCalledTimes(1); + }); - // test('should be ok with no storage provider', () => { - // const sessionReplay = new SessionReplayPlugin(); - // destination.config = useDefaultConfig(); - // destination.config.storageProvider = undefined; - // expect(destination.saveEvents()).toBe(undefined); - // }); - // }); + test('should send later', async () => { + const sessionReplay = new SessionReplayPlugin(); + sessionReplay.config = mockConfig; + sessionReplay.queue = [ + { + events: [mockEvent], + sequenceId: 1, + sessionId: 123, + attempts: 0, + timeout: 1000, + }, + ]; + const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); + const result = await sessionReplay.flush(); + expect(sessionReplay.queue).toEqual([ + { + events: [mockEvent], + sequenceId: 1, + sessionId: 123, + attempts: 0, + timeout: 1000, + }, + ]); + expect(result).toBe(undefined); + expect(send).toHaveBeenCalledTimes(0); + }); + }); - // describe('module level integration', () => { - // const successResponse = { - // status: Status.Success, - // statusCode: 200, - // body: { - // eventsIngested: 1, - // payloadSizeBytes: 1, - // serverUploadTime: 1, - // }, - // }; + describe('send', () => { + test('should make a request correctly', async () => { + const sessionReplay = new SessionReplayPlugin(); + sessionReplay.config = mockConfig; + const context = { + events: [mockEvent], + sequenceId: 1, + sessionId: 123, + attempts: 0, + timeout: 0, + }; - // test('should handle unexpected error', async () => { - // class Http { - // send = jest.fn().mockImplementationOnce(() => { - // return Promise.resolve(null); - // }); - // } - // const transportProvider = new Http(); - // const sessionReplay = new SessionReplayPlugin(); - // const config = { - // ...useDefaultConfig(), - // flushQueueSize: 2, - // flushIntervalMillis: 500, - // transportProvider, - // }; - // await destination.setup(config); - // const result = await destination.execute({ - // event_type: 'event_type', - // }); - // expect(result.code).toBe(0); - // expect(result.message).toBe(UNEXPECTED_ERROR_MESSAGE); - // expect(transportProvider.send).toHaveBeenCalledTimes(1); - // }); + await sessionReplay.send(context); + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith('https://api-secure.amplitude.com/sessions/track', { + body: JSON.stringify({ + api_key: 'static_key', + session_id: 123, + start_timestamp: 123, + events_batch: { version: 1, events: [mockEvent], seq_number: 1 }, + }), + headers: { Accept: '*/*', 'Content-Type': 'application/json' }, + method: 'POST', + }); + }); + test('should remove session events from IDB store upon success', async () => { + const sessionReplay = new SessionReplayPlugin(); + sessionReplay.config = mockConfig; + const context = { + events: [mockEvent], + sequenceId: 1, + sessionId: 123, + attempts: 0, + timeout: 0, + }; + await sessionReplay.send(context); + jest.runAllTimers(); + const mockIDBStore = { + 123: { + events: [mockEvent], + sequenceId: 3, + }, + 456: { + events: [mockEvent], + sequenceId: 1, + }, + }; - // test('should not retry with invalid api key', async () => { - // class Http { - // send = jest.fn().mockImplementationOnce(() => { - // return Promise.resolve({ - // status: Status.Invalid, - // statusCode: 400, - // body: { - // error: INVALID_API_KEY, - // }, - // }); - // }); - // } - // const transportProvider = new Http(); - // const sessionReplay = new SessionReplayPlugin(); - // destination.retryTimeout = 10; - // const config = { - // ...useDefaultConfig(), - // flushQueueSize: 2, - // flushIntervalMillis: 500, - // transportProvider, - // }; - // await destination.setup(config); - // const results = await Promise.all([ - // destination.execute({ - // event_type: 'event_type', - // }), - // destination.execute({ - // event_type: 'event_type', - // }), - // ]); - // expect(results[0].code).toBe(400); - // expect(transportProvider.send).toHaveBeenCalledTimes(1); - // }); + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ + 456: { + events: [mockEvent], + sequenceId: 1, + }, + }); + }); + test('should not remove session events from IDB store upon failure', async () => { + const sessionReplay = new SessionReplayPlugin(); + sessionReplay.config = mockConfig; + const context = { + events: [mockEvent], + sequenceId: 1, + sessionId: 123, + attempts: 0, + timeout: 0, + }; + (global.fetch as jest.Mock).mockImplementationOnce(() => Promise.reject()); - // test('should handle retry for 400 error', async () => { - // class Http { - // send = jest - // .fn() - // .mockImplementationOnce(() => { - // return Promise.resolve({ - // status: Status.Invalid, - // statusCode: 400, - // body: { - // error: 'error', - // missingField: '', - // eventsWithInvalidFields: { a: [0] }, - // eventsWithMissingFields: { b: [] }, - // eventsWithInvalidIdLengths: {}, - // silencedEvents: [], - // }, - // }); - // }) - // .mockImplementationOnce(() => { - // return Promise.resolve(successResponse); - // }); - // } - // const transportProvider = new Http(); - // const sessionReplay = new SessionReplayPlugin(); - // destination.retryTimeout = 10; - // const config = { - // ...useDefaultConfig(), - // flushQueueSize: 2, - // flushIntervalMillis: 500, - // transportProvider, - // }; - // await destination.setup(config); - // const results = await Promise.all([ - // destination.execute({ - // event_type: 'event_type', - // }), - // destination.execute({ - // event_type: 'event_type', - // }), - // ]); - // expect(results[0].code).toBe(400); - // expect(results[1].code).toBe(200); - // expect(transportProvider.send).toHaveBeenCalledTimes(2); - // }); + await sessionReplay.send(context); + jest.runAllTimers(); - // test('should handle retry for 400 error with missing body field', async () => { - // class Http { - // send = jest.fn().mockImplementationOnce(() => { - // return Promise.resolve({ - // status: Status.Invalid, - // statusCode: 400, - // body: { - // error: 'error', - // missingField: 'key', - // eventsWithInvalidFields: {}, - // eventsWithMissingFields: {}, - // silencedEvents: [], - // }, - // }); - // }); - // } - // const transportProvider = new Http(); - // const sessionReplay = new SessionReplayPlugin(); - // const config = { - // ...useDefaultConfig(), - // flushQueueSize: 2, - // flushIntervalMillis: 500, - // transportProvider, - // }; - // await destination.setup(config); - // const results = await Promise.all([ - // destination.execute({ - // event_type: 'event_type', - // }), - // destination.execute({ - // event_type: 'event_type', - // }), - // ]); - // expect(results[0].code).toBe(400); - // expect(results[1].code).toBe(400); - // expect(transportProvider.send).toHaveBeenCalledTimes(1); - // }); + expect(update).not.toHaveBeenCalled(); + }); - // test('should handle retry for 413 error with flushQueueSize of 1', async () => { - // class Http { - // send = jest.fn().mockImplementationOnce(() => { - // return Promise.resolve({ - // status: Status.PayloadTooLarge, - // statusCode: 413, - // body: { - // error: 'error', - // }, - // }); - // }); - // } - // const transportProvider = new Http(); - // const sessionReplay = new SessionReplayPlugin(); - // const config = { - // ...useDefaultConfig(), - // flushQueueSize: 1, - // flushIntervalMillis: 500, - // transportProvider, - // }; - // await destination.setup(config); - // const event = { - // event_type: 'event_type', - // }; - // const result = await destination.execute(event); - // expect(result).toEqual({ - // event, - // message: 'error', - // code: 413, - // }); - // expect(transportProvider.send).toHaveBeenCalledTimes(1); - // }); + test('should retry if retry param is true', async () => { + const sessionReplay = new SessionReplayPlugin(); + sessionReplay.config = mockConfig; + const context = { + events: [mockEvent], + sequenceId: 1, + sessionId: 123, + attempts: 0, + timeout: 0, + }; + (global.fetch as jest.Mock) + .mockImplementationOnce(() => + Promise.resolve({ + status: 500, + }), + ) + .mockImplementationOnce(() => + Promise.resolve({ + status: 200, + }), + ); + const addToQueue = jest.spyOn(sessionReplay, 'addToQueue'); - // test('should handle retry for 413 error', async () => { - // class Http { - // send = jest - // .fn() - // .mockImplementationOnce(() => { - // return Promise.resolve({ - // status: Status.PayloadTooLarge, - // statusCode: 413, - // body: { - // error: 'error', - // }, - // }); - // }) - // .mockImplementationOnce(() => { - // return Promise.resolve(successResponse); - // }) - // .mockImplementationOnce(() => { - // return Promise.resolve(successResponse); - // }); - // } - // const transportProvider = new Http(); - // const sessionReplay = new SessionReplayPlugin(); - // destination.retryTimeout = 10; - // const config = { - // ...useDefaultConfig(), - // flushQueueSize: 2, - // flushIntervalMillis: 500, - // transportProvider, - // }; - // await destination.setup(config); - // await Promise.all([ - // destination.execute({ - // event_type: 'event_type', - // }), - // destination.execute({ - // event_type: 'event_type', - // }), - // ]); - // expect(transportProvider.send).toHaveBeenCalledTimes(3); - // }); + await sessionReplay.send(context, true); + expect(addToQueue).toHaveBeenCalledTimes(1); + expect(addToQueue).toHaveBeenCalledWith({ + ...context, + attempts: 1, + timeout: 0, + }); + await runScheduleTimers(); + }); - // test('should handle retry for 429 error', async () => { - // class Http { - // send = jest - // .fn() - // .mockImplementationOnce(() => { - // return Promise.resolve({ - // status: Status.RateLimit, - // statusCode: 429, - // body: { - // error: 'error', - // epsThreshold: 1, - // throttledDevices: {}, - // throttledUsers: {}, - // exceededDailyQuotaDevices: { - // '1': 1, - // }, - // exceededDailyQuotaUsers: { - // '2': 1, - // }, - // throttledEvents: [0], - // }, - // }); - // }) - // .mockImplementationOnce(() => { - // return Promise.resolve(successResponse); - // }) - // .mockImplementationOnce(() => { - // return Promise.resolve(successResponse); - // }); - // } - // const transportProvider = new Http(); - // const sessionReplay = new SessionReplayPlugin(); - // destination.retryTimeout = 10; - // destination.throttleTimeout = 1; - // const config = { - // ...useDefaultConfig(), - // flushQueueSize: 4, - // flushIntervalMillis: 500, - // transportProvider, - // }; - // await destination.setup(config); - // const results = await Promise.all([ - // // throttled - // destination.execute({ - // event_type: 'event_type', - // user_id: '0', - // device_id: '0', - // }), - // // exceed daily device quota - // destination.execute({ - // event_type: 'event_type', - // user_id: '1', - // device_id: '1', - // }), - // // exceed daily user quota - // destination.execute({ - // event_type: 'event_type', - // user_id: '2', - // device_id: '2', - // }), - // // success - // destination.execute({ - // event_type: 'event_type', - // user_id: '3', - // device_id: '3', - // }), - // ]); - // expect(results[0].code).toBe(200); - // expect(results[1].code).toBe(429); - // expect(results[2].code).toBe(429); - // expect(results[3].code).toBe(200); - // expect(transportProvider.send).toHaveBeenCalledTimes(2); - // }); + test('should not retry if retry param is false', async () => { + const sessionReplay = new SessionReplayPlugin(); + sessionReplay.config = mockConfig; + const context = { + events: [mockEvent], + sequenceId: 1, + sessionId: 123, + attempts: 0, + timeout: 0, + }; + (global.fetch as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ + status: 500, + }), + ); + const addToQueue = jest.spyOn(sessionReplay, 'addToQueue'); - // test('should handle retry for 500 error', async () => { - // class Http { - // send = jest - // .fn() - // .mockImplementationOnce(() => { - // return Promise.resolve({ - // statusCode: 500, - // status: Status.Failed, - // }); - // }) - // .mockImplementationOnce(() => { - // return Promise.resolve(successResponse); - // }); - // } - // const transportProvider = new Http(); - // const sessionReplay = new SessionReplayPlugin(); - // destination.retryTimeout = 10; - // const config = { - // ...useDefaultConfig(), - // flushQueueSize: 2, - // flushIntervalMillis: 500, - // transportProvider, - // }; - // await destination.setup(config); - // await Promise.all([ - // destination.execute({ - // event_type: 'event_type', - // }), - // destination.execute({ - // event_type: 'event_type', - // }), - // ]); - // expect(transportProvider.send).toHaveBeenCalledTimes(2); - // }); + await sessionReplay.send(context, false); + expect(addToQueue).toHaveBeenCalledTimes(0); + }); + }); - // test('should handle retry for 503 error', async () => { - // class Http { - // send = jest - // .fn() - // .mockImplementationOnce(() => { - // return Promise.resolve({ - // statusCode: 500, - // status: Status.Failed, - // }); - // }) - // .mockImplementationOnce(() => { - // return Promise.resolve({ - // statusCode: 500, - // status: Status.Failed, - // }); - // }); - // } - // const transportProvider = new Http(); - // const sessionReplay = new SessionReplayPlugin(); - // destination.retryTimeout = 10; - // const config = { - // ...useDefaultConfig(), - // flushMaxRetries: 1, - // flushQueueSize: 2, - // flushIntervalMillis: 500, - // transportProvider, - // }; - // await destination.setup(config); - // const results = await Promise.all([ - // destination.execute({ - // event_type: 'event_type', - // }), - // destination.execute({ - // event_type: 'event_type', - // }), - // ]); - // expect(results[0].code).toBe(500); - // expect(results[1].code).toBe(500); - // expect(transportProvider.send).toHaveBeenCalledTimes(1); - // }); - // }); + describe('module level integration', () => { + const mockLoggerProvider = { + error: jest.fn(), + log: jest.fn(), + logLevel: 1, + disable: jest.fn(), + enable: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }; + test('should handle unexpected error', async () => { + const sessionReplay = new SessionReplayPlugin(); + const config = { + ...mockConfig, + loggerProvider: mockLoggerProvider, + }; + (fetch as jest.Mock).mockImplementationOnce(() => Promise.reject('API Failure')); + await sessionReplay.setup(config); + await sessionReplay.execute({ + event_type: 'session_end', + session_id: 456, + }); + await runScheduleTimers(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual('API Failure'); + }); + test('should handle retry for 400 error', async () => { + (fetch as jest.Mock) + .mockImplementationOnce(() => { + return Promise.resolve({ + status: 400, + }); + }) + .mockImplementationOnce(() => { + return Promise.resolve({ + status: 200, + }); + }); + const sessionReplay = new SessionReplayPlugin(); + sessionReplay.retryTimeout = 10; + const config = { + ...mockConfig, + flushMaxRetries: 2, + loggerProvider: mockLoggerProvider, + }; + await sessionReplay.setup(config); + // Log is called from setup, but that's not what we're testing here + mockLoggerProvider.log.mockClear(); + await sessionReplay.execute({ + event_type: 'session_end', + session_id: 456, + }); + await runScheduleTimers(); + expect(fetch).toHaveBeenCalledTimes(2); + expect(mockLoggerProvider.error).toHaveBeenCalledTimes(0); + expect(mockLoggerProvider.log).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(mockLoggerProvider.log.mock.calls[0][0]).toEqual('Session Replay events tracked successfully'); + }); + test('should handle retry for 413 error with flushQueueSize of 1', async () => { + (fetch as jest.Mock) + .mockImplementationOnce(() => { + return Promise.resolve({ + status: 413, + }); + }) + .mockImplementationOnce(() => { + return Promise.resolve({ + status: 200, + }); + }); + const sessionReplay = new SessionReplayPlugin(); + const config = { + ...mockConfig, + flushMaxRetries: 2, + }; + await sessionReplay.setup(config); + const event = { + event_type: 'session_end', + session_id: 456, + }; + await sessionReplay.execute(event); + await runScheduleTimers(); + expect(fetch).toHaveBeenCalledTimes(2); + }); + test('should handle retry for 500 error', async () => { + (fetch as jest.Mock) + .mockImplementationOnce(() => { + return Promise.resolve({ + status: 500, + }); + }) + .mockImplementationOnce(() => { + return Promise.resolve({ + status: 200, + }); + }); + const sessionReplay = new SessionReplayPlugin(); + const config = { + ...mockConfig, + flushMaxRetries: 2, + }; + await sessionReplay.setup(config); + const event = { + event_type: 'session_end', + session_id: 456, + }; + await sessionReplay.execute(event); + await runScheduleTimers(); + expect(fetch).toHaveBeenCalledTimes(2); + }); + test('should handle retry for 503 error', async () => { + (fetch as jest.Mock) + .mockImplementationOnce(() => { + return Promise.resolve({ + status: 503, + }); + }) + .mockImplementationOnce(() => { + return Promise.resolve({ + status: 200, + }); + }); + const sessionReplay = new SessionReplayPlugin(); + const config = { + ...mockConfig, + flushMaxRetries: 2, + }; + await sessionReplay.setup(config); + const event = { + event_type: 'session_end', + session_id: 456, + }; + await sessionReplay.execute(event); + await runScheduleTimers(); + expect(fetch).toHaveBeenCalledTimes(2); + }); + }); }); From f9d98cd1ad24a191bb265bb16d6dd88201de7823 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 22 Jun 2023 13:11:06 -0400 Subject: [PATCH 031/214] feat(plugins): cleanup and update readme and changelog --- packages/plugin-session-replay/CHANGELOG.md | 5 +- packages/plugin-session-replay/README.md | 80 +++---------------- packages/plugin-session-replay/src/helpers.ts | 1 - .../plugin-session-replay/src/messages.ts | 4 - .../src/session-replay.ts | 5 +- .../test/session-replay.test.ts | 4 +- yarn.lock | 5 -- 7 files changed, 19 insertions(+), 85 deletions(-) diff --git a/packages/plugin-session-replay/CHANGELOG.md b/packages/plugin-session-replay/CHANGELOG.md index b67c38fc7..ea145a276 100644 --- a/packages/plugin-session-replay/CHANGELOG.md +++ b/packages/plugin-session-replay/CHANGELOG.md @@ -3,9 +3,8 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -# 0.1.0 (2022-09-16) +# 0.1.0 (2023-06-22) ### Features -- new marketing analytics plugin ([#213](https://github.com/amplitude/Amplitude-TypeScript/issues/213)) - ([02ff174](https://github.com/amplitude/Amplitude-TypeScript/commit/02ff174e3361173dbf15ed3acf72e950810e174f)) +- new session replay plugin ([#393](https://github.com/amplitude/Amplitude-TypeScript/pull/393)) diff --git a/packages/plugin-session-replay/README.md b/packages/plugin-session-replay/README.md index 2cffd9ced..b8e56e84c 100644 --- a/packages/plugin-session-replay/README.md +++ b/packages/plugin-session-replay/README.md @@ -5,9 +5,9 @@

-# @amplitude/plugin-web-attribution-browser +# @amplitude/plugin-session-replay -Official Browser SDK plugin for web attribution tracking +Official Browser SDK plugin for session replay ## Installation @@ -15,86 +15,32 @@ This package is published on NPM registry and is available to be installed using ```sh # npm -npm install @amplitude/plugin-web-attribution-browser +npm install @amplitude/plugin-session-replay # yarn -yarn add @amplitude/plugin-web-attribution-browser +yarn add @amplitude/plugin-session-replay ``` ## Usage -This plugin works on top of Amplitude Browser SDK and adds web attribution tracking features to built-in features. To use this plugin, you need to install `@amplitude/analytics-browser` version `v1.4.0` or later. +This plugin works on top of Amplitude Browser SDK and adds session replay features to built-in features. To use this plugin, you need to install `@amplitude/analytics-browser` version `v2.0.0` or later. ### 1. Import Amplitude packages * `@amplitude/analytics-browser` -* `@amplitude/plugin-web-attribution-browser` +* `@amplitude/plugin-session-replay` ```typescript import * as amplitude from '@amplitude/analytics-browser'; -import { webAttributionPlugin } from '@amplitude/plugin-web-attribution-browser'; +import { SessionReplayPlugin } from '@amplitude/plugin-session-replay'; ``` -### 2. Instantiate page view plugin +### 2. Instantiate session replay plugin and install plugin to Amplitude SDK -The plugin requires 1 parameter, which is the `amplitude` instance. The plugin also accepts an optional second parameter, which is an `Object` to configure the plugin based on your use case. +The plugin must be registered with the amplitude instance via the following code: ```typescript -const webAttributionTracking = webAttributionPlugin(amplitude, { - disabled: undefined, - excludeReferrers: undefined, - initialEmptyValue: undefined, - resetSessionOnNewCampaign: undefined, -}); -``` - -#### Options - -|Name|Type|Default|Description| -|-|-|-|-| -|`disabled`|`boolean`|`false`|Use this option to enable or disable web attribution tracking. By default, upon installing this plugin, web attribution tracking is enabled.| -|`excludeReferrers`|`string[]`|`[]`|Use this option to prevent the plugin from tracking campaigns parameters from specific referrers. For example: `subdomain.domain.com`.| -|`initialEmptyValue`|`string`|`"EMPTY"`|Use this option to specify empty values for [first-touch attribution](https://www.docs.developers.amplitude.com/data/sdks/marketing-analytics-browser/#first-touch-attribution).| -|`resetSessionOnNewCampaign`|`boolean`|`false`|Use this option to control whether a new session should start on a new campaign.| - -### 3. Install plugin to Amplitude SDK - -```typescript -amplitude.add(webAttributionTracking); -``` - -### 4. Initialize Amplitude SDK - -```typescript -amplitude.init('API_KEY'); -``` - -## Resulting web attribution event - -This plugin tracks campaign parameters based on your configuration. A web attribution event is composed of the following values: - -#### Event type -* `"$idenfity"` - -#### User properties - -|Property|Description| -|-|-| -|`utm_source`|URL query parameter value for `utm_source`| -|`utm_medium`|URL query parameter value for `utm_medium`| -|`utm_campaign`|URL query parameter value for `utm_campaign`| -|`utm_term`|URL query parameter value for `utm_term`| -|`utm_content`|URL query parameter value for `utm_content`| -|`referrer`|Referring webstite or `document.referrer`| -|`referring_domain`|Referring website's domain, including subdomain| -|`dclid`|URL query parameter value for `dclid`| -|`gbraid`|URL query parameter value for `gbraid`| -|`gclid`|URL query parameter value for `gclid`| -|`fbclid`|URL query parameter value for `fbclid`| -|`ko_click_id`|URL query parameter value for `ko_click_id`| -|`li_fat_id`|URL query parameter value for `li_fat_id`| -|`msclkid`|URL query parameter value for `msclkid`| -|`rtd_cid`|URL query parameter value for `rtd_cid`| -|`ttclid`|URL query parameter value for `ttclid`| -|`twclid`|URL query parameter value for `twclid`| -|`wbraid`|URL query parameter value for `wbraid`| +amplitude.init(API_KEY) +const sessionReplayTracking = new SessionReplayPlugin(); +amplitude.add(sessionReplayTracking) +``` \ No newline at end of file diff --git a/packages/plugin-session-replay/src/helpers.ts b/packages/plugin-session-replay/src/helpers.ts index e2c0c189b..f6b2025e5 100644 --- a/packages/plugin-session-replay/src/helpers.ts +++ b/packages/plugin-session-replay/src/helpers.ts @@ -1,6 +1,5 @@ const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; -// const MAX_EVENT_LIST_SIZE_IN_BYTES = 80000; export const shouldSplitEventsList = (eventsList: string[], nextEventString: string): boolean => { const sizeOfNextEvent = new Blob([nextEventString]).size; diff --git a/packages/plugin-session-replay/src/messages.ts b/packages/plugin-session-replay/src/messages.ts index 898cdc023..8b93b23da 100644 --- a/packages/plugin-session-replay/src/messages.ts +++ b/packages/plugin-session-replay/src/messages.ts @@ -1,8 +1,4 @@ export const SUCCESS_MESSAGE = 'Session replay event batch tracked successfully'; export const UNEXPECTED_ERROR_MESSAGE = 'Unexpected error occurred'; export const MAX_RETRIES_EXCEEDED_MESSAGE = 'Session replay event batch rejected due to exceeded retry count'; -export const OPT_OUT_MESSAGE = 'Event skipped due to optOut config'; -export const MISSING_API_KEY_MESSAGE = 'Session replay event batch rejected due to missing API key'; -export const INVALID_API_KEY = 'Invalid API key'; -export const CLIENT_NOT_INITIALIZED = 'Client not initialized'; export const STORAGE_FAILURE = 'Failed to store session replay events in IndexedDB'; diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts index 0dd9db568..cbb47a174 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -3,7 +3,7 @@ import { BrowserConfig, EnrichmentPlugin, Event, PluginType, Status } from '@amp import * as IDBKeyVal from 'idb-keyval'; import { record } from 'rrweb'; import { shouldSplitEventsList } from './helpers'; -import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, UNEXPECTED_ERROR_MESSAGE } from './messages'; +import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from './messages'; import { Events, IDBStore, SessionReplayContext } from './typings/session-replay'; const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/track'; @@ -111,7 +111,6 @@ export class SessionReplayPlugin implements EnrichmentPlugin { context.attempts += 1; return true; } - // TODO: should we keep events in indexdb to retry on a refresh? Whats destination behavior? this.completeRequest({ context, err: `${MAX_RETRIES_EXCEEDED_MESSAGE}, batch sequence id, ${context.sequenceId}`, @@ -210,7 +209,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { } handleSuccessResponse(context: SessionReplayContext) { - this.completeRequest({ context, success: `Session Replay events tracked successfully` }); + this.completeRequest({ context, success: SUCCESS_MESSAGE }); } handleOtherResponse(context: SessionReplayContext) { diff --git a/packages/plugin-session-replay/test/session-replay.test.ts b/packages/plugin-session-replay/test/session-replay.test.ts index 00e9fb3d1..1370114b4 100644 --- a/packages/plugin-session-replay/test/session-replay.test.ts +++ b/packages/plugin-session-replay/test/session-replay.test.ts @@ -1,8 +1,8 @@ -/* eslint-disable jest/expect-expect */ import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; import { BrowserConfig, LogLevel } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import * as RRWeb from 'rrweb'; +import { SUCCESS_MESSAGE } from '../src/messages'; import { SessionReplayPlugin } from '../src/session-replay'; jest.mock('idb-keyval'); @@ -493,7 +493,7 @@ describe('SessionReplayPlugin', () => { expect(mockLoggerProvider.error).toHaveBeenCalledTimes(0); expect(mockLoggerProvider.log).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.log.mock.calls[0][0]).toEqual('Session Replay events tracked successfully'); + expect(mockLoggerProvider.log.mock.calls[0][0]).toEqual(SUCCESS_MESSAGE); }); test('should handle retry for 413 error with flushQueueSize of 1', async () => { (fetch as jest.Mock) diff --git a/yarn.lock b/yarn.lock index 7770d2e1e..33a296cca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7378,11 +7378,6 @@ iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -idb-keyval@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33" - integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg== - ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" From e371754434984ac44cc9462d5eac114d6142c938 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 22 Jun 2023 14:59:03 -0400 Subject: [PATCH 032/214] feat(plugins): complete test coverage --- packages/plugin-session-replay/src/helpers.ts | 7 +- .../src/session-replay.ts | 47 +-- .../test/session-replay.test.ts | 270 +++++++++++++++--- 3 files changed, 260 insertions(+), 64 deletions(-) diff --git a/packages/plugin-session-replay/src/helpers.ts b/packages/plugin-session-replay/src/helpers.ts index f6b2025e5..67d59de84 100644 --- a/packages/plugin-session-replay/src/helpers.ts +++ b/packages/plugin-session-replay/src/helpers.ts @@ -1,10 +1,7 @@ -const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events -const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; - -export const shouldSplitEventsList = (eventsList: string[], nextEventString: string): boolean => { +export const shouldSplitEventsList = (eventsList: string[], nextEventString: string, maxListSize: number): boolean => { const sizeOfNextEvent = new Blob([nextEventString]).size; const sizeOfEventsList = new Blob(eventsList).size; - if (sizeOfEventsList + sizeOfNextEvent >= MAX_EVENT_LIST_SIZE_IN_BYTES) { + if (sizeOfEventsList + sizeOfNextEvent >= maxListSize) { return true; } return false; diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts index cbb47a174..b7c5f766f 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -8,6 +8,9 @@ import { Events, IDBStore, SessionReplayContext } from './typings/session-replay const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/track'; const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; +const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events +const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; + export class SessionReplayPlugin implements EnrichmentPlugin { name = '@amplitude/plugin-session-replay'; type = PluginType.ENRICHMENT as const; @@ -22,6 +25,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { private scheduled: ReturnType | null = null; queue: SessionReplayContext[] = []; stopRecordingEvents: ReturnType | null = null; + maxPersistedEventsSize = MAX_EVENT_LIST_SIZE_IN_BYTES; async setup(config: BrowserConfig) { config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); @@ -71,7 +75,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.events = []; const currentSessionStoredEvents = this.config.sessionId && storedReplaySessions[this.config.sessionId]; this.currentSequenceId = currentSessionStoredEvents ? currentSessionStoredEvents.sequenceId + 1 : 0; - this.storeEventsForSession([], this.currentSequenceId); + void this.storeEventsForSession([], this.currentSequenceId); } } @@ -79,7 +83,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.stopRecordingEvents = record({ emit: (event) => { const eventString = JSON.stringify(event); - const shouldSplit = shouldSplitEventsList(this.events, eventString); + const shouldSplit = shouldSplitEventsList(this.events, eventString, this.maxPersistedEventsSize); if (shouldSplit) { this.sendEventsList({ events: this.events, @@ -90,7 +94,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.currentSequenceId++; } this.events.push(eventString); - this.storeEventsForSession(this.events, this.currentSequenceId); + void this.storeEventsForSession(this.events, this.currentSequenceId); }, }); } @@ -180,6 +184,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { const res = await fetch(SESSION_REPLAY_SERVER_URL, options); if (res === null) { this.completeRequest({ context, err: UNEXPECTED_ERROR_MESSAGE, removeEvents: false }); + return; } if (!useRetry) { let responseBody = ''; @@ -230,36 +235,32 @@ export class SessionReplayPlugin implements EnrichmentPlugin { return undefined; } - storeEventsForSession(events: Events, sequenceId: number) { + async storeEventsForSession(events: Events, sequenceId: number) { try { - void IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore | undefined): IDBStore => { - if (this.config.sessionId) { - return { - ...sessionMap, + await IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore | undefined): IDBStore => { + return { + ...sessionMap, + ...(this.config.sessionId && { [this.config.sessionId]: { events: events, sequenceId, }, - }; - } - return sessionMap || {}; + }), + }; }); } catch (e) { this.config.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); } } - removeSessionEventsStore(sessionId: number | undefined) { - if (sessionId) { - try { - void IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore | undefined): IDBStore => { - sessionMap = sessionMap || {}; - delete sessionMap[sessionId]; - return sessionMap; - }); - } catch (e) { - this.config.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); - } + async removeSessionEventsStore(sessionId: number) { + try { + await IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore = {}): IDBStore => { + delete sessionMap[sessionId]; + return sessionMap; + }); + } catch (e) { + this.config.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); } } @@ -274,7 +275,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { success?: string; removeEvents?: boolean; }) { - removeEvents && this.removeSessionEventsStore(context.sessionId); + removeEvents && context.sessionId && this.removeSessionEventsStore(context.sessionId); if (err) { this.config.loggerProvider.error(err); } else if (success) { diff --git a/packages/plugin-session-replay/test/session-replay.test.ts b/packages/plugin-session-replay/test/session-replay.test.ts index 1370114b4..3cc661bab 100644 --- a/packages/plugin-session-replay/test/session-replay.test.ts +++ b/packages/plugin-session-replay/test/session-replay.test.ts @@ -1,8 +1,10 @@ +/* eslint-disable jest/expect-expect */ import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; import { BrowserConfig, LogLevel } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import * as RRWeb from 'rrweb'; -import { SUCCESS_MESSAGE } from '../src/messages'; +import { shouldSplitEventsList } from '../src/helpers'; +import { SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from '../src/messages'; import { SessionReplayPlugin } from '../src/session-replay'; jest.mock('idb-keyval'); @@ -11,8 +13,12 @@ type MockedIDBKeyVal = jest.Mocked; jest.mock('rrweb'); type MockedRRWeb = jest.Mocked; -const mockEvent = - '{"type":4,"data":{"href":"https://analytics.amplitude.com/","width":1728,"height":154},"timestamp":1687358660935}'; +const mockEvent = { + type: 4, + data: { href: 'https://analytics.amplitude.com/', width: 1728, height: 154 }, + timestamp: 1687358660935, +}; +const mockEventString = JSON.stringify(mockEvent); async function runScheduleTimers() { // exhause first setTimeout @@ -23,6 +29,16 @@ async function runScheduleTimers() { jest.runAllTimers(); } +const mockLoggerProvider = { + error: jest.fn(), + log: jest.fn(), + logLevel: 1, + disable: jest.fn(), + enable: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), +}; + describe('SessionReplayPlugin', () => { const { get, update } = IDBKeyVal as MockedIDBKeyVal; const { record } = RRWeb as MockedRRWeb; @@ -45,7 +61,7 @@ describe('SessionReplayPlugin', () => { serverUrl: 'url', transportProvider: new FetchTransport(), useBatch: false, - + sessionId: 123, cookieExpiration: 365, cookieSameSite: 'Lax', cookieSecure: false, @@ -90,30 +106,32 @@ describe('SessionReplayPlugin', () => { expect(sessionReplay.storageKey).toBe('AMP_replay_unsent_static_key'); }); - // polluting fetch test('should read events from storage and send them, then reset storage for session', async () => { const sessionReplay = new SessionReplayPlugin(); - mockConfig.sessionId = 456; + const config = { + ...mockConfig, + sessionId: 456, + }; get.mockResolvedValueOnce({ 123: { - events: [mockEvent], + events: [mockEventString], sequenceId: 3, }, 456: { - events: [mockEvent], + events: [mockEventString], sequenceId: 1, }, }); const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); - await sessionReplay.setup(mockConfig); + await sessionReplay.setup(config); jest.runAllTimers(); expect(send).toHaveBeenCalledTimes(2); // Sending first stored session events expect(send.mock.calls[0][0]).toEqual({ attempts: 1, - events: [mockEvent], + events: [mockEventString], sequenceId: 3, sessionId: 123, timeout: 0, @@ -121,7 +139,7 @@ describe('SessionReplayPlugin', () => { // Sending second stored session events expect(send.mock.calls[1][0]).toEqual({ attempts: 1, - events: [mockEvent], + events: [mockEventString], sequenceId: 1, sessionId: 456, timeout: 0, @@ -135,6 +153,22 @@ describe('SessionReplayPlugin', () => { }, }); }); + test('should handle no stored events', async () => { + const sessionReplay = new SessionReplayPlugin(); + const config = { + ...mockConfig, + }; + get.mockResolvedValueOnce({}); + await sessionReplay.setup(config); + expect(sessionReplay.currentSequenceId).toBe(0); + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][1]({})).toEqual({ + '123': { + events: [], + sequenceId: 0, + }, + }); + }); test('should record events', async () => { const sessionReplay = new SessionReplayPlugin(); await sessionReplay.setup(mockConfig); @@ -186,7 +220,7 @@ describe('SessionReplayPlugin', () => { event_type: 'session_end', session_id: 789, }; - sessionReplay.events = [mockEvent]; + sessionReplay.events = [mockEventString]; await sessionReplay.execute(event); jest.runAllTimers(); expect(send).toHaveBeenCalledTimes(1); @@ -194,7 +228,7 @@ describe('SessionReplayPlugin', () => { // Sending first stored session events expect(send.mock.calls[0][0]).toEqual({ attempts: 1, - events: [mockEvent], + events: [mockEventString], sequenceId: 0, sessionId: 789, timeout: 0, @@ -202,13 +236,62 @@ describe('SessionReplayPlugin', () => { }); }); + describe('recordEvents', () => { + test('should store events in class and in IDB', () => { + const sessionReplay = new SessionReplayPlugin(); + sessionReplay.config = mockConfig; + sessionReplay.recordEvents(); + expect(sessionReplay.events).toEqual([]); + const recordArg = record.mock.calls[0][0]; + recordArg?.emit && recordArg?.emit(mockEvent); + expect(sessionReplay.events).toEqual([mockEventString]); + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][1]({})).toEqual({ + 123: { + events: [mockEventString], + sequenceId: 0, + }, + }); + }); + + test('should split the events list and send', () => { + const sessionReplay = new SessionReplayPlugin(); + sessionReplay.config = mockConfig; + sessionReplay.maxPersistedEventsSize = 20; + const events = ['#'.repeat(20)]; + sessionReplay.events = events; + // eslint-disable-next-line @typescript-eslint/no-empty-function + const sendEventsListMock = jest.spyOn(sessionReplay, 'sendEventsList').mockImplementationOnce(() => {}); + sessionReplay.recordEvents(); + + const recordArg = record.mock.calls[0][0]; + recordArg?.emit && recordArg?.emit(mockEvent); + + expect(sendEventsListMock).toHaveBeenCalledTimes(1); + expect(sendEventsListMock).toHaveBeenCalledWith({ + events, + sequenceId: 0, + sessionId: 123, + }); + + expect(sessionReplay.events).toEqual([mockEventString]); + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][1]({})).toEqual({ + 123: { + events: [mockEventString], + sequenceId: 1, + }, + }); + }); + }); + describe('addToQueue', () => { test('should add to queue and schedule a flush', () => { const sessionReplay = new SessionReplayPlugin(); sessionReplay.config = mockConfig; const schedule = jest.spyOn(sessionReplay, 'schedule').mockReturnValueOnce(undefined); const context = { - events: [mockEvent], + events: [mockEventString], sequenceId: 1, sessionId: 123, attempts: 0, @@ -218,6 +301,34 @@ describe('SessionReplayPlugin', () => { expect(schedule).toHaveBeenCalledTimes(1); expect(context.attempts).toBe(1); }); + + test('should not add to queue if attemps are greater than allowed retries', () => { + const sessionReplay = new SessionReplayPlugin(); + sessionReplay.config = { + ...mockConfig, + flushMaxRetries: 1, + }; + const completeRequest = jest.spyOn(sessionReplay, 'completeRequest').mockReturnValueOnce(undefined); + const context = { + events: [mockEventString], + sequenceId: 1, + sessionId: 123, + attempts: 1, + timeout: 0, + }; + sessionReplay.addToQueue(context); + expect(completeRequest).toHaveBeenCalledTimes(1); + expect(completeRequest).toHaveBeenCalledWith({ + context: { + events: [mockEventString], + sequenceId: 1, + sessionId: 123, + attempts: 1, + timeout: 0, + }, + err: 'Session replay event batch rejected due to exceeded retry count, batch sequence id, 1', + }); + }); }); describe('schedule', () => { @@ -227,7 +338,7 @@ describe('SessionReplayPlugin', () => { (sessionReplay as any).scheduled = null; sessionReplay.queue = [ { - events: [mockEvent], + events: [mockEventString], sequenceId: 1, sessionId: 123, attempts: 0, @@ -263,7 +374,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; sessionReplay.queue = [ { - events: [mockEvent], + events: [mockEventString], sequenceId: 1, sessionId: 123, attempts: 0, @@ -282,7 +393,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; sessionReplay.queue = [ { - events: [mockEvent], + events: [mockEventString], sequenceId: 1, sessionId: 123, attempts: 0, @@ -293,7 +404,7 @@ describe('SessionReplayPlugin', () => { const result = await sessionReplay.flush(); expect(sessionReplay.queue).toEqual([ { - events: [mockEvent], + events: [mockEventString], sequenceId: 1, sessionId: 123, attempts: 0, @@ -310,7 +421,7 @@ describe('SessionReplayPlugin', () => { const sessionReplay = new SessionReplayPlugin(); sessionReplay.config = mockConfig; const context = { - events: [mockEvent], + events: [mockEventString], sequenceId: 1, sessionId: 123, attempts: 0, @@ -324,7 +435,7 @@ describe('SessionReplayPlugin', () => { api_key: 'static_key', session_id: 123, start_timestamp: 123, - events_batch: { version: 1, events: [mockEvent], seq_number: 1 }, + events_batch: { version: 1, events: [mockEventString], seq_number: 1 }, }), headers: { Accept: '*/*', 'Content-Type': 'application/json' }, method: 'POST', @@ -334,7 +445,7 @@ describe('SessionReplayPlugin', () => { const sessionReplay = new SessionReplayPlugin(); sessionReplay.config = mockConfig; const context = { - events: [mockEvent], + events: [mockEventString], sequenceId: 1, sessionId: 123, attempts: 0, @@ -344,11 +455,11 @@ describe('SessionReplayPlugin', () => { jest.runAllTimers(); const mockIDBStore = { 123: { - events: [mockEvent], + events: [mockEventString], sequenceId: 3, }, 456: { - events: [mockEvent], + events: [mockEventString], sequenceId: 1, }, }; @@ -356,7 +467,7 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ 456: { - events: [mockEvent], + events: [mockEventString], sequenceId: 1, }, }); @@ -365,7 +476,7 @@ describe('SessionReplayPlugin', () => { const sessionReplay = new SessionReplayPlugin(); sessionReplay.config = mockConfig; const context = { - events: [mockEvent], + events: [mockEventString], sequenceId: 1, sessionId: 123, attempts: 0, @@ -383,7 +494,7 @@ describe('SessionReplayPlugin', () => { const sessionReplay = new SessionReplayPlugin(); sessionReplay.config = mockConfig; const context = { - events: [mockEvent], + events: [mockEventString], sequenceId: 1, sessionId: 123, attempts: 0, @@ -416,7 +527,7 @@ describe('SessionReplayPlugin', () => { const sessionReplay = new SessionReplayPlugin(); sessionReplay.config = mockConfig; const context = { - events: [mockEvent], + events: [mockEventString], sequenceId: 1, sessionId: 123, attempts: 0, @@ -435,15 +546,6 @@ describe('SessionReplayPlugin', () => { }); describe('module level integration', () => { - const mockLoggerProvider = { - error: jest.fn(), - log: jest.fn(), - logLevel: 1, - disable: jest.fn(), - enable: jest.fn(), - warn: jest.fn(), - debug: jest.fn(), - }; test('should handle unexpected error', async () => { const sessionReplay = new SessionReplayPlugin(); const config = { @@ -573,5 +675,101 @@ describe('SessionReplayPlugin', () => { await runScheduleTimers(); expect(fetch).toHaveBeenCalledTimes(2); }); + test('should handle unexpected error where fetch response is null', async () => { + (fetch as jest.Mock).mockImplementationOnce(() => { + return Promise.resolve(null); + }); + const sessionReplay = new SessionReplayPlugin(); + const config = { + ...mockConfig, + flushMaxRetries: 2, + loggerProvider: mockLoggerProvider, + }; + await sessionReplay.setup(config); + const event = { + event_type: 'session_end', + session_id: 456, + }; + await sessionReplay.execute(event); + await runScheduleTimers(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual(UNEXPECTED_ERROR_MESSAGE); + }); + }); + + describe('idb error handling', () => { + test('getAllSessionEventsFromStore should catch errors', async () => { + const sessionReplay = new SessionReplayPlugin(); + const config = { + ...mockConfig, + loggerProvider: mockLoggerProvider, + }; + sessionReplay.config = config; + get.mockImplementationOnce(() => Promise.reject('error')); + await sessionReplay.getAllSessionEventsFromStore(); + expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( + 'Failed to store session replay events in IndexedDB: error', + ); + }); + test('storeEventsForSession should catch errors', async () => { + const sessionReplay = new SessionReplayPlugin(); + const config = { + ...mockConfig, + loggerProvider: mockLoggerProvider, + }; + sessionReplay.config = config; + update.mockImplementationOnce(() => Promise.reject('error')); + await sessionReplay.storeEventsForSession([mockEventString], 0); + expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( + 'Failed to store session replay events in IndexedDB: error', + ); + }); + test('removeSessionEventsStore should catch errors', async () => { + const sessionReplay = new SessionReplayPlugin(); + const config = { + ...mockConfig, + loggerProvider: mockLoggerProvider, + }; + sessionReplay.config = config; + update.mockImplementationOnce(() => Promise.reject('error')); + await sessionReplay.removeSessionEventsStore(123); + expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( + 'Failed to store session replay events in IndexedDB: error', + ); + }); + test('removeSessionEventsStore should handle an undefined store', async () => { + const sessionReplay = new SessionReplayPlugin(); + const config = { + ...mockConfig, + loggerProvider: mockLoggerProvider, + }; + sessionReplay.config = config; + update.mockImplementationOnce(() => Promise.resolve()); + await sessionReplay.removeSessionEventsStore(123); + expect(update.mock.calls[0][1](undefined)).toEqual({}); + }); + }); + + describe('shouldSplitEventsList', () => { + test('shouldSplitEventsList should return true if size of events list plus size of next event is over the max size', () => { + const eventsList = ['#'.repeat(20)]; + const nextEvent = 'a'; + const result = shouldSplitEventsList(eventsList, nextEvent, 20); + expect(result).toBe(true); + }); + test('shouldSplitEventsList should return false if size of events list plus size of next event is under the max size', () => { + const eventsList = ['#'.repeat(20)]; + const nextEvent = 'a'; + const result = shouldSplitEventsList(eventsList, nextEvent, 22); + expect(result).toBe(false); + }); }); }); From 1365bae315076eecd8568fabbc72dc07efcfaf82 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Fri, 23 Jun 2023 13:29:10 -0400 Subject: [PATCH 033/214] feat(plugins): add packFn to session replay --- packages/plugin-session-replay/src/session-replay.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts index b7c5f766f..b687751c9 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -1,7 +1,7 @@ import { AMPLITUDE_PREFIX, BaseTransport } from '@amplitude/analytics-core'; import { BrowserConfig, EnrichmentPlugin, Event, PluginType, Status } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; -import { record } from 'rrweb'; +import { pack, record } from 'rrweb'; import { shouldSplitEventsList } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from './messages'; import { Events, IDBStore, SessionReplayContext } from './typings/session-replay'; @@ -96,6 +96,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.events.push(eventString); void this.storeEventsForSession(this.events, this.currentSequenceId); }, + packFn: pack, }); } From 1b8f037bd6913f71db73f97e39175858bf7b1954 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Mon, 26 Jun 2023 13:37:40 -0400 Subject: [PATCH 034/214] feat(plugins): pr feedback --- packages/plugin-session-replay/CHANGELOG.md | 10 ---------- packages/plugin-session-replay/README.md | 6 +++--- packages/plugin-session-replay/package.json | 13 +++++++------ packages/plugin-session-replay/src/index.ts | 1 - .../src/typings/session-replay.ts | 9 --------- 5 files changed, 10 insertions(+), 29 deletions(-) delete mode 100644 packages/plugin-session-replay/CHANGELOG.md diff --git a/packages/plugin-session-replay/CHANGELOG.md b/packages/plugin-session-replay/CHANGELOG.md deleted file mode 100644 index ea145a276..000000000 --- a/packages/plugin-session-replay/CHANGELOG.md +++ /dev/null @@ -1,10 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# 0.1.0 (2023-06-22) - -### Features - -- new session replay plugin ([#393](https://github.com/amplitude/Amplitude-TypeScript/pull/393)) diff --git a/packages/plugin-session-replay/README.md b/packages/plugin-session-replay/README.md index b8e56e84c..a1ed2a77e 100644 --- a/packages/plugin-session-replay/README.md +++ b/packages/plugin-session-replay/README.md @@ -23,16 +23,16 @@ yarn add @amplitude/plugin-session-replay ## Usage -This plugin works on top of Amplitude Browser SDK and adds session replay features to built-in features. To use this plugin, you need to install `@amplitude/analytics-browser` version `v2.0.0` or later. +This plugin works on top of Amplitude Browser SDK and adds session replay features to built-in features. To use this plugin, you need to install `@amplitude/analytics-browser` version `v1.0.0` or later. ### 1. Import Amplitude packages * `@amplitude/analytics-browser` -* `@amplitude/plugin-session-replay` +* `@amplitude/plugin-session-replay-browser` ```typescript import * as amplitude from '@amplitude/analytics-browser'; -import { SessionReplayPlugin } from '@amplitude/plugin-session-replay'; +import { SessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'; ``` ### 2. Instantiate session replay plugin and install plugin to Amplitude SDK diff --git a/packages/plugin-session-replay/package.json b/packages/plugin-session-replay/package.json index be02e5817..eff7229dd 100644 --- a/packages/plugin-session-replay/package.json +++ b/packages/plugin-session-replay/package.json @@ -1,6 +1,6 @@ { - "name": "@amplitude/plugin-session-replay", - "version": "1.0.0-beta.0", + "name": "@amplitude/plugin-session-replay-browser", + "version": "0.0.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -10,7 +10,8 @@ "types": "lib/esm/index.d.ts", "sideEffects": false, "publishConfig": { - "access": "public" + "access": "public", + "tag": "beta" }, "repository": { "type": "git", @@ -36,14 +37,14 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.0-beta.0", - "@amplitude/analytics-types": "^1.0.0-beta.0", + "@amplitude/analytics-client-common": ">=1 <3", + "@amplitude/analytics-types": ">=1 <3", "idb-keyval": "^6.2.1", "rrweb": "^2.0.0-alpha.4", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.10.3", + "@amplitude/analytics-browser": ">=1 <3", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", diff --git a/packages/plugin-session-replay/src/index.ts b/packages/plugin-session-replay/src/index.ts index 4a1605e03..7ec717f3b 100644 --- a/packages/plugin-session-replay/src/index.ts +++ b/packages/plugin-session-replay/src/index.ts @@ -1,2 +1 @@ export { SessionReplayPlugin as Plugin, SessionReplayPlugin } from './session-replay'; -export * as Types from './typings/session-replay'; diff --git a/packages/plugin-session-replay/src/typings/session-replay.ts b/packages/plugin-session-replay/src/typings/session-replay.ts index 12329dc8e..21445cbb5 100644 --- a/packages/plugin-session-replay/src/typings/session-replay.ts +++ b/packages/plugin-session-replay/src/typings/session-replay.ts @@ -1,12 +1,3 @@ -/* eslint-disable @typescript-eslint/no-empty-interface */ -import { EnrichmentPlugin } from '@amplitude/analytics-types'; - -export interface Options {} - -export interface CreateSessionReplayPlugin { - (options?: Options): EnrichmentPlugin; -} - export type Events = string[]; export interface SessionReplayContext { From 84b349c868308a759204dc7dbf9115bd5ee69ab3 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Mon, 26 Jun 2023 15:16:45 -0400 Subject: [PATCH 035/214] feat(plugins): update yarn.lock after changing branch base --- yarn.lock | 88 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index 33a296cca..0a43256b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@amplitude/analytics-connector@^1.4.8": +"@amplitude/analytics-connector@^1.4.5": version "1.4.8" resolved "https://registry.yarnpkg.com/@amplitude/analytics-connector/-/analytics-connector-1.4.8.tgz#dd801303db2662bc51be7e0194eeb8bd72267c42" integrity sha512-dFW7c7Wb6Ng7vbmzwbaXZSpqfBx37ukamJV9ErFYYS8vGZK/Hkbt3M7fZHBI4WFU6CCwakr2ZXPme11uGPYWkQ== @@ -3778,6 +3778,13 @@ estree-walker "^2.0.2" picomatch "^2.3.1" +"@rrweb/types@^2.0.0-alpha.4": + version "2.0.0-alpha.8" + resolved "https://registry.yarnpkg.com/@rrweb/types/-/types-2.0.0-alpha.8.tgz#29a6550dd833364a459af4be4ce3b233003fb78b" + integrity sha512-yAr6ZQrgmr7+qZU5DMGqYXnVsolC5epftmZtkOtgFD/bbvCWflNnl09M32hUjttlKCV1ohhmQGioXkCQ37IF7A== + dependencies: + rrweb-snapshot "^2.0.0-alpha.8" + "@sideway/address@^4.1.3": version "4.1.4" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" @@ -3872,6 +3879,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/css-font-loading-module@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz#2f98ede46acc0975de85c0b7b0ebe06041d24601" + integrity sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q== + "@types/estree@*", "@types/estree@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" @@ -4125,6 +4137,11 @@ "@typescript-eslint/types" "5.46.1" eslint-visitor-keys "^3.3.0" +"@xstate/fsm@^1.4.0": + version "1.6.5" + resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.6.5.tgz#f599e301997ad7e3c572a0b1ff0696898081bea5" + integrity sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw== + "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" @@ -5129,6 +5146,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-arraybuffer@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== + base64-js@^1.1.2, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -6690,6 +6712,11 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fflate@^0.4.4: + version "0.4.8" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" + integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== + figures@3.2.0, figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -7378,6 +7405,11 @@ iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +idb-keyval@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33" + integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg== + ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -9447,6 +9479,11 @@ minizlib@^2.1.1, minizlib@^2.1.2: minipass "^3.0.0" yallist "^4.0.0" +mitt@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd" + integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ== + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -9559,10 +9596,10 @@ nocache@^3.0.1: resolved "https://registry.yarnpkg.com/nocache/-/nocache-3.0.4.tgz#5b37a56ec6e09fc7d401dceaed2eab40c8bfdf79" integrity sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw== -nock@^13.2.4: - version "13.2.9" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.2.9.tgz#4faf6c28175d36044da4cfa68e33e5a15086ad4c" - integrity sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA== +nock@^13.2.4, nock@^13.2.9: + version "13.3.1" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.3.1.tgz#f22d4d661f7a05ebd9368edae1b5dc0a62d758fc" + integrity sha512-vHnopocZuI93p2ccivFyGuUfzjq2fxNyNurp7816mlT5V5HF4SzXu8lvLrVzBbNqzs+ODooZ6OksuSUNM7Njkw== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" @@ -10980,6 +11017,32 @@ rollup@^2.79.1: optionalDependencies: fsevents "~2.3.2" +rrdom@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/rrdom/-/rrdom-0.1.7.tgz#f2f49bfd01b59291bb7b0d981371a5e02a18e2aa" + integrity sha512-ZLd8f14z9pUy2Hk9y636cNv5Y2BMnNEY99wxzW9tD2BLDfe1xFxtLjB4q/xCBYo6HRe0wofzKzjm4JojmpBfFw== + dependencies: + rrweb-snapshot "^2.0.0-alpha.4" + +rrweb-snapshot@^2.0.0-alpha.4, rrweb-snapshot@^2.0.0-alpha.8: + version "2.0.0-alpha.8" + resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.8.tgz#5f93f8ab7d78b3de26f6a64e993ff68edd54e0cf" + integrity sha512-3Rb7c+mnDEADQ8N9qn9SDH5PzCyHlZ1cwZC932qRyt9O8kJWLM11JLYqqEyQCa2FZVQbzH2iAaCgnyM7A32p7A== + +rrweb@^2.0.0-alpha.4: + version "2.0.0-alpha.4" + resolved "https://registry.yarnpkg.com/rrweb/-/rrweb-2.0.0-alpha.4.tgz#3c7cf2f1bcf44f7a88dd3fad00ee8d6dd711f258" + integrity sha512-wEHUILbxDPcNwkM3m4qgPgXAiBJyqCbbOHyVoNEVBJzHszWEFYyTbrZqUdeb1EfmTRC2PsumCIkVcomJ/xcOzA== + dependencies: + "@rrweb/types" "^2.0.0-alpha.4" + "@types/css-font-loading-module" "0.0.7" + "@xstate/fsm" "^1.4.0" + base64-arraybuffer "^1.0.1" + fflate "^0.4.4" + mitt "^3.0.0" + rrdom "^0.1.7" + rrweb-snapshot "^2.0.0-alpha.4" + run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -12495,20 +12558,7 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.0, yargs@^17.3.1, yargs@^17.5.1, yargs@^17.6.2: - version "17.6.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" - integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - -yargs@^17.7.1: +yargs@^17.0.0, yargs@^17.3.1, yargs@^17.5.1, yargs@^17.6.2, yargs@^17.7.1: version "17.7.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967" integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw== From 83ce8371f8edea800ff6ac11c6184d8cc1bb17c7 Mon Sep 17 00:00:00 2001 From: Andrey Sokolov Date: Mon, 26 Jun 2023 23:29:51 +0400 Subject: [PATCH 036/214] fix: add app_set_id, idfa and idfv support (#456) * fix: added support for Android app set ID * fix: add support for idfa/idfv * fix: change argument type for getApplicationContext() native method * fix: remove direct support for idfa, add plugin example with idfa support * fix: use a 3rd party package in example idfa plugin --- .../react-native-idfa-plugin/idfaPlugin.ts | 25 +++++++++++++++++++ .../reactnative/AmplitudeReactNativeModule.kt | 8 +++--- .../ios/AmplitudeReactNative.m | 2 +- .../ios/AmplitudeReactNative.swift | 13 +++++++--- .../ios/AppleContextProvider.swift | 12 +++++++++ packages/analytics-react-native/src/config.ts | 2 ++ .../src/plugins/context.ts | 18 ++++++++++--- .../test/config.test.ts | 6 +++++ .../test/helpers/default.ts | 2 ++ .../test/plugins/context.test.ts | 5 ++++ packages/analytics-types/src/base-event.ts | 1 + .../src/config/react-native.ts | 2 ++ 12 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 examples/plugins/react-native-idfa-plugin/idfaPlugin.ts diff --git a/examples/plugins/react-native-idfa-plugin/idfaPlugin.ts b/examples/plugins/react-native-idfa-plugin/idfaPlugin.ts new file mode 100644 index 000000000..52cda28e7 --- /dev/null +++ b/examples/plugins/react-native-idfa-plugin/idfaPlugin.ts @@ -0,0 +1,25 @@ +import { Types } from '@amplitude/analytics-react-native'; +import ReactNativeIdfaAaid from '@sparkfabrik/react-native-idfa-aaid'; + +export default class IdfaPlugin implements Types.BeforePlugin { + name = 'idfa'; + type = Types.PluginType.BEFORE as any; + idfa: string | null = null; + + async setup(_config: Types.Config): Promise { + try { + const info = await ReactNativeIdfaAaid.getAdvertisingInfo(); + this.idfa = info.id; + } catch (e) { + console.log(e); + } + return undefined; + } + + async execute(context: Types.Event): Promise { + if (this.idfa) { + context.idfa = this.idfa; + } + return context; + } +} diff --git a/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AmplitudeReactNativeModule.kt b/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AmplitudeReactNativeModule.kt index d42085c9c..e8fb30c16 100644 --- a/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AmplitudeReactNativeModule.kt +++ b/packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/AmplitudeReactNativeModule.kt @@ -20,9 +20,10 @@ class AmplitudeReactNativeModule(private val reactContext: ReactApplicationConte } @ReactMethod - private fun getApplicationContext(shouldTrackAdid: Boolean, promise: Promise) { + private fun getApplicationContext(options: ReadableMap, promise: Promise) { + val trackAdid = if (options.hasKey("adid")) options.getBoolean("adid") else false if (androidContextProvider == null) { - androidContextProvider = AndroidContextProvider(reactContext.applicationContext, false, shouldTrackAdid) + androidContextProvider = AndroidContextProvider(reactContext.applicationContext, false, trackAdid) } promise.resolve(WritableNativeMap().apply { @@ -35,9 +36,10 @@ class AmplitudeReactNativeModule(private val reactContext: ReactApplicationConte putString("deviceManufacturer", androidContextProvider!!.manufacturer) putString("deviceModel", androidContextProvider!!.model) putString("carrier", androidContextProvider!!.carrier) - if (androidContextProvider!!.advertisingId != null) { + if (trackAdid) { putString("adid", androidContextProvider!!.advertisingId) } + putString("appSetId", androidContextProvider!!.appSetId) }) } } diff --git a/packages/analytics-react-native/ios/AmplitudeReactNative.m b/packages/analytics-react-native/ios/AmplitudeReactNative.m index 3c9a587a2..8f3aa47f2 100644 --- a/packages/analytics-react-native/ios/AmplitudeReactNative.m +++ b/packages/analytics-react-native/ios/AmplitudeReactNative.m @@ -2,6 +2,6 @@ @interface RCT_EXTERN_MODULE(AmplitudeReactNative, NSObject) -RCT_EXTERN_METHOD(getApplicationContext: (BOOL)shouldTrackAdid resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(getApplicationContext: (NSDictionary*)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) @end diff --git a/packages/analytics-react-native/ios/AmplitudeReactNative.swift b/packages/analytics-react-native/ios/AmplitudeReactNative.swift index d1d8d51a5..282a9aba4 100644 --- a/packages/analytics-react-native/ios/AmplitudeReactNative.swift +++ b/packages/analytics-react-native/ios/AmplitudeReactNative.swift @@ -4,8 +4,6 @@ import React @objc(AmplitudeReactNative) class ReactNative: NSObject { - private let appleContextProvider = AppleContextProvider() - @objc static func requiresMainQueueSetup() -> Bool { return false @@ -13,11 +11,15 @@ class ReactNative: NSObject { @objc func getApplicationContext( - _ shouldTrackAdid: Bool, + _ options: NSDictionary, resolver resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock ) -> Void { - let applicationContext: [String: String?] = [ + let trackingOptions = options as! [String: Bool] + let trackIdfv = trackingOptions["idfv"] ?? false + let appleContextProvider = AppleContextProvider(trackIdfv: trackIdfv) + + var applicationContext: [String: String?] = [ "version": appleContextProvider.version, "platform": appleContextProvider.platform, "language": appleContextProvider.language, @@ -26,6 +28,9 @@ class ReactNative: NSObject { "deviceManufacturer": appleContextProvider.deviceManufacturer, "deviceModel": appleContextProvider.deviceModel, ] + if (trackIdfv) { + applicationContext["idfv"] = appleContextProvider.idfv + } resolve(applicationContext) } } diff --git a/packages/analytics-react-native/ios/AppleContextProvider.swift b/packages/analytics-react-native/ios/AppleContextProvider.swift index 12367d511..31ca43706 100644 --- a/packages/analytics-react-native/ios/AppleContextProvider.swift +++ b/packages/analytics-react-native/ios/AppleContextProvider.swift @@ -9,6 +9,14 @@ import Foundation public let osVersion: String = AppleContextProvider.getOsVersion() public let deviceManufacturer: String = AppleContextProvider.getDeviceManufacturer() public let deviceModel: String = AppleContextProvider.getDeviceModel() + public var idfv: String? = nil + + init(trackIdfv: Bool) { + super.init() + if (trackIdfv) { + fetchIdfv() + } + } private static func getVersion() -> String? { return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String @@ -41,6 +49,10 @@ import Foundation return String(bytes: Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN)), encoding: .ascii)!.trimmingCharacters(in: .controlCharacters) } + private func fetchIdfv() { + self.idfv = UIDevice.current.identifierForVendor?.uuidString + } + private static func getDeviceModel() -> String { let platform = getPlatformString() // == iPhone == diff --git a/packages/analytics-react-native/src/config.ts b/packages/analytics-react-native/src/config.ts index 6b8b9880e..d0c80e192 100644 --- a/packages/analytics-react-native/src/config.ts +++ b/packages/analytics-react-native/src/config.ts @@ -23,6 +23,8 @@ export const getDefaultConfig = () => { osName: true, osVersion: true, platform: true, + appSetId: true, + idfv: true, }; return { cookieExpiration: 365, diff --git a/packages/analytics-react-native/src/plugins/context.ts b/packages/analytics-react-native/src/plugins/context.ts index 08391e616..8c4414a44 100644 --- a/packages/analytics-react-native/src/plugins/context.ts +++ b/packages/analytics-react-native/src/plugins/context.ts @@ -1,4 +1,10 @@ -import { BeforePlugin, ReactNativeConfig, Event, PluginType } from '@amplitude/analytics-types'; +import { + BeforePlugin, + ReactNativeConfig, + Event, + PluginType, + ReactNativeTrackingOptions, +} from '@amplitude/analytics-types'; import UAParser from '@amplitude/ua-parser-js'; import { UUID } from '@amplitude/analytics-core'; import { getLanguage } from '@amplitude/analytics-client-common'; @@ -19,10 +25,12 @@ type NativeContext = { deviceModel: string; carrier: string; adid: string; + appSetId: string; + idfv: string; }; export interface AmplitudeReactNative { - getApplicationContext(shouldTrackAdid: boolean): Promise; + getApplicationContext(options: ReactNativeTrackingOptions): Promise; } export class Context implements BeforePlugin { @@ -55,7 +63,7 @@ export class Context implements BeforePlugin { async execute(context: Event): Promise { const time = new Date().getTime(); - const nativeContext = await this.nativeModule?.getApplicationContext(this.config.trackingOptions.adid ?? false); + const nativeContext = await this.nativeModule?.getApplicationContext(this.config.trackingOptions); const appVersion = nativeContext?.version || this.config.appVersion; const platform = nativeContext?.platform || BROWSER_PLATFORM; const osName = nativeContext?.osName || this.uaResult.browser.name; @@ -65,6 +73,8 @@ export class Context implements BeforePlugin { const language = nativeContext?.language || getLanguage(); const carrier = nativeContext?.carrier; const adid = nativeContext?.adid; + const appSetId = nativeContext?.appSetId; + const idfv = nativeContext?.idfv; const event: Event = { user_id: this.config.userId, @@ -81,6 +91,8 @@ export class Context implements BeforePlugin { ...(this.config.trackingOptions.carrier && { carrier: carrier }), ...(this.config.trackingOptions.ipAddress && { ip: IP_ADDRESS }), ...(this.config.trackingOptions.adid && { adid: adid }), + ...(this.config.trackingOptions.appSetId && { android_app_set_id: appSetId }), + ...(this.config.trackingOptions.idfv && { idfv: idfv }), insert_id: UUID(), partner_id: this.config.partnerId, plan: this.config.plan, diff --git a/packages/analytics-react-native/test/config.test.ts b/packages/analytics-react-native/test/config.test.ts index 458ba04d6..cc2470b29 100644 --- a/packages/analytics-react-native/test/config.test.ts +++ b/packages/analytics-react-native/test/config.test.ts @@ -55,6 +55,8 @@ describe('config', () => { osName: true, osVersion: true, platform: true, + appSetId: true, + idfv: true, }, transportProvider: new FetchTransport(), useBatch: false, @@ -106,6 +108,8 @@ describe('config', () => { osName: true, osVersion: true, platform: true, + appSetId: true, + idfv: true, }, transportProvider: new FetchTransport(), useBatch: false, @@ -185,6 +189,8 @@ describe('config', () => { osName: true, osVersion: true, platform: true, + appSetId: true, + idfv: true, }, transportProvider: new FetchTransport(), useBatch: false, diff --git a/packages/analytics-react-native/test/helpers/default.ts b/packages/analytics-react-native/test/helpers/default.ts index ebf714a4b..6ebf51175 100644 --- a/packages/analytics-react-native/test/helpers/default.ts +++ b/packages/analytics-react-native/test/helpers/default.ts @@ -38,6 +38,8 @@ export const DEFAULT_OPTIONS: Partial = { osName: true, osVersion: true, platform: true, + appSetId: true, + idfv: true, }, transportProvider: { send: () => Promise.resolve(null), diff --git a/packages/analytics-react-native/test/plugins/context.test.ts b/packages/analytics-react-native/test/plugins/context.test.ts index 679386bec..65e6ae06b 100644 --- a/packages/analytics-react-native/test/plugins/context.test.ts +++ b/packages/analytics-react-native/test/plugins/context.test.ts @@ -72,6 +72,8 @@ describe('context', () => { osName: false, osVersion: false, platform: false, + appSetId: false, + idfv: false, }, userId: 'user@amplitude.com', }); @@ -92,6 +94,9 @@ describe('context', () => { expect(firstContextEvent.os_version).toBeUndefined(); expect(firstContextEvent.language).toBeUndefined(); expect(firstContextEvent.ip).toBeUndefined(); + expect(firstContextEvent.adid).toBeUndefined(); + expect(firstContextEvent.android_app_set_id).toBeUndefined(); + expect(firstContextEvent.idfv).toBeUndefined(); expect(firstContextEvent.device_id).toEqual('deviceId'); expect(firstContextEvent.session_id).toEqual(1); expect(firstContextEvent.user_id).toEqual('user@amplitude.com'); diff --git a/packages/analytics-types/src/base-event.ts b/packages/analytics-types/src/base-event.ts index 615667299..8585c3fb3 100644 --- a/packages/analytics-types/src/base-event.ts +++ b/packages/analytics-types/src/base-event.ts @@ -47,5 +47,6 @@ export interface EventOptions { ingestion_metadata?: IngestionMetadataEventProperty; partner_id?: string; user_agent?: string; + android_app_set_id?: string; extra?: { [key: string]: any }; } diff --git a/packages/analytics-types/src/config/react-native.ts b/packages/analytics-types/src/config/react-native.ts index da508a9d7..67fb7c911 100644 --- a/packages/analytics-types/src/config/react-native.ts +++ b/packages/analytics-types/src/config/react-native.ts @@ -9,6 +9,8 @@ export interface ReactNativeConfig extends BrowserConfig { export interface ReactNativeTrackingOptions extends TrackingOptions { adid?: boolean; carrier?: boolean; + appSetId?: boolean; + idfv?: boolean; } type HiddenOptions = 'apiKey' | 'lastEventId'; From 72f4348e07d6cc065442f431032bc10ae4a095a7 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Mon, 26 Jun 2023 15:58:34 -0400 Subject: [PATCH 037/214] feat(plugins): adjust timing of setup to capture early sent events --- .../plugin-session-replay/src/session-replay.ts | 6 +++--- .../test/session-replay.test.ts | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts index b687751c9..e70209910 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -32,9 +32,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { this.config = config; this.storageKey = `${STORAGE_PREFIX}_${this.config.apiKey.substring(0, 10)}`; - await this.emptyStoreAndReset(); - - this.recordEvents(); + void this.emptyStoreAndReset(); } execute(event: Event) { @@ -61,6 +59,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { async emptyStoreAndReset() { const storedReplaySessions = await this.getAllSessionEventsFromStore(); + console.log('in empty store after await'); if (storedReplaySessions) { for (const sessionId in storedReplaySessions) { const storedReplayEvents = storedReplaySessions[sessionId]; @@ -76,6 +75,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { const currentSessionStoredEvents = this.config.sessionId && storedReplaySessions[this.config.sessionId]; this.currentSequenceId = currentSessionStoredEvents ? currentSessionStoredEvents.sequenceId + 1 : 0; void this.storeEventsForSession([], this.currentSequenceId); + this.recordEvents(); } } diff --git a/packages/plugin-session-replay/test/session-replay.test.ts b/packages/plugin-session-replay/test/session-replay.test.ts index 3cc661bab..195f980dc 100644 --- a/packages/plugin-session-replay/test/session-replay.test.ts +++ b/packages/plugin-session-replay/test/session-replay.test.ts @@ -112,7 +112,7 @@ describe('SessionReplayPlugin', () => { ...mockConfig, sessionId: 456, }; - get.mockResolvedValueOnce({ + const mockGetResolution = Promise.resolve({ 123: { events: [mockEventString], sequenceId: 3, @@ -122,9 +122,11 @@ describe('SessionReplayPlugin', () => { sequenceId: 1, }, }); + get.mockReturnValueOnce(mockGetResolution); const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); await sessionReplay.setup(config); + await mockGetResolution; jest.runAllTimers(); expect(send).toHaveBeenCalledTimes(2); @@ -158,8 +160,10 @@ describe('SessionReplayPlugin', () => { const config = { ...mockConfig, }; - get.mockResolvedValueOnce({}); + const mockGetResolution = Promise.resolve({}); + get.mockReturnValueOnce(mockGetResolution); await sessionReplay.setup(config); + await mockGetResolution; expect(sessionReplay.currentSequenceId).toBe(0); expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1]({})).toEqual({ @@ -170,8 +174,11 @@ describe('SessionReplayPlugin', () => { }); }); test('should record events', async () => { + const mockGetResolution = Promise.resolve({}); + get.mockReturnValueOnce(mockGetResolution); const sessionReplay = new SessionReplayPlugin(); await sessionReplay.setup(mockConfig); + await mockGetResolution; jest.runAllTimers(); expect(record).toHaveBeenCalledTimes(1); }); @@ -201,7 +208,10 @@ describe('SessionReplayPlugin', () => { const sessionReplay = new SessionReplayPlugin(); const mockStopRecordingEvents = jest.fn(); record.mockReturnValue(mockStopRecordingEvents); + const mockGetResolution = Promise.resolve({}); + get.mockReturnValueOnce(mockGetResolution); await sessionReplay.setup(mockConfig); + await mockGetResolution; jest.runAllTimers(); const event = { event_type: 'session_start', From 01c20d61e8c51b59dc7fdf83d4abd57391aecf71 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Mon, 26 Jun 2023 22:09:09 +0000 Subject: [PATCH 038/214] chore(release): publish - @amplitude/analytics-browser-test@1.4.6 - @amplitude/analytics-browser@1.12.0 - @amplitude/analytics-client-common@1.1.0 - @amplitude/analytics-core@1.2.0 - @amplitude/analytics-node-test@1.0.4 - @amplitude/analytics-node@1.3.0 - @amplitude/analytics-react-native@1.3.0 - @amplitude/analytics-types@1.3.0 - @amplitude/marketing-analytics-browser@1.0.6 - @amplitude/plugin-page-view-tracking-browser@1.0.6 - @amplitude/plugin-web-attribution-browser@1.0.6 --- packages/analytics-browser-test/CHANGELOG.md | 9 ++++++++ packages/analytics-browser-test/package.json | 4 ++-- packages/analytics-browser/CHANGELOG.md | 13 ++++++++++++ packages/analytics-browser/README.md | 2 +- .../generated/amplitude-snippet.js | 4 ++-- packages/analytics-browser/package.json | 12 +++++------ .../analytics-browser/playground/amplitude.js | 2 +- packages/analytics-browser/src/version.ts | 2 +- packages/analytics-client-common/CHANGELOG.md | 13 ++++++++++++ packages/analytics-client-common/package.json | 6 +++--- packages/analytics-core/CHANGELOG.md | 13 ++++++++++++ packages/analytics-core/package.json | 4 ++-- packages/analytics-node-test/CHANGELOG.md | 9 ++++++++ packages/analytics-node-test/package.json | 4 ++-- packages/analytics-node/CHANGELOG.md | 13 ++++++++++++ packages/analytics-node/package.json | 6 +++--- packages/analytics-node/src/version.ts | 2 +- packages/analytics-react-native/CHANGELOG.md | 18 ++++++++++++++++ packages/analytics-react-native/package.json | 8 +++---- .../analytics-react-native/src/version.ts | 2 +- packages/analytics-types/CHANGELOG.md | 21 +++++++++++++++++++ packages/analytics-types/package.json | 2 +- .../marketing-analytics-browser/CHANGELOG.md | 9 ++++++++ .../marketing-analytics-browser/README.md | 2 +- .../generated/amplitude-gtm-snippet.js | 4 ++-- .../generated/amplitude-snippet.js | 4 ++-- .../marketing-analytics-browser/package.json | 14 ++++++------- .../src/version.ts | 2 +- .../CHANGELOG.md | 9 ++++++++ .../package.json | 8 +++---- .../CHANGELOG.md | 9 ++++++++ .../package.json | 8 +++---- 32 files changed, 187 insertions(+), 51 deletions(-) diff --git a/packages/analytics-browser-test/CHANGELOG.md b/packages/analytics-browser-test/CHANGELOG.md index 754f26251..03ef9016c 100644 --- a/packages/analytics-browser-test/CHANGELOG.md +++ b/packages/analytics-browser-test/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.4.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.5...@amplitude/analytics-browser-test@1.4.6) (2023-06-26) + +**Note:** Version bump only for package @amplitude/analytics-browser-test + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.4.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.4...@amplitude/analytics-browser-test@1.4.5) (2023-06-14) **Note:** Version bump only for package @amplitude/analytics-browser-test diff --git a/packages/analytics-browser-test/package.json b/packages/analytics-browser-test/package.json index 914059eaf..96b473245 100644 --- a/packages/analytics-browser-test/package.json +++ b/packages/analytics-browser-test/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser-test", - "version": "1.4.5", + "version": "1.4.6", "private": true, "description": "", "author": "Amplitude Inc", @@ -16,7 +16,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.11.2" + "@amplitude/analytics-browser": "^1.12.0" }, "devDependencies": { "nock": "^13.2.4" diff --git a/packages/analytics-browser/CHANGELOG.md b/packages/analytics-browser/CHANGELOG.md index 646aea545..a8b538e4b 100644 --- a/packages/analytics-browser/CHANGELOG.md +++ b/packages/analytics-browser/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.12.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.11.2...@amplitude/analytics-browser@1.12.0) (2023-06-26) + +### Features + +- add option for instance name ([#428](https://github.com/amplitude/Amplitude-TypeScript/issues/428)) + ([#442](https://github.com/amplitude/Amplitude-TypeScript/issues/442)) + ([569464c](https://github.com/amplitude/Amplitude-TypeScript/commit/569464c698eb54b3da05e203ac90cf1a399d96ed)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.11.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.11.1...@amplitude/analytics-browser@1.11.2) (2023-06-14) **Note:** Version bump only for package @amplitude/analytics-browser diff --git a/packages/analytics-browser/README.md b/packages/analytics-browser/README.md index 7332fe927..a43a16b2c 100644 --- a/packages/analytics-browser/README.md +++ b/packages/analytics-browser/README.md @@ -40,7 +40,7 @@ Alternatively, the package is also distributed through a CDN. Copy and paste the ```html diff --git a/packages/analytics-browser/generated/amplitude-snippet.js b/packages/analytics-browser/generated/amplitude-snippet.js index c3d50c9ef..6c6c42737 100644 --- a/packages/analytics-browser/generated/amplitude-snippet.js +++ b/packages/analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-bhExRHNdxBGFQ+AkmAurYWjqboez+iJTTaWsIubqgMeSgQZqjXMa8BI8890ijsCf'; + as.integrity = 'sha384-s4Wx3hZn20lk10XSCWAIMwlpJRoowQ1Yt6E0gUEhrmytlmyODAskmfnOcu84UYAg'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.11.2-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.12.0-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/analytics-browser/package.json b/packages/analytics-browser/package.json index 388ae14e8..d73e6b7e7 100644 --- a/packages/analytics-browser/package.json +++ b/packages/analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser", - "version": "1.11.2", + "version": "1.12.0", "description": "Official Amplitude SDK for Web", "keywords": [ "analytics", @@ -44,11 +44,11 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.3", - "@amplitude/analytics-core": "^1.1.2", - "@amplitude/analytics-types": "^1.2.1", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.5", - "@amplitude/plugin-web-attribution-browser": "^1.0.5", + "@amplitude/analytics-client-common": "^1.1.0", + "@amplitude/analytics-core": "^1.2.0", + "@amplitude/analytics-types": "^1.3.0", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.6", + "@amplitude/plugin-web-attribution-browser": "^1.0.6", "@amplitude/ua-parser-js": "^0.7.31", "tslib": "^2.4.1" }, diff --git a/packages/analytics-browser/playground/amplitude.js b/packages/analytics-browser/playground/amplitude.js index 9d8797e08..17b09d826 100644 --- a/packages/analytics-browser/playground/amplitude.js +++ b/packages/analytics-browser/playground/amplitude.js @@ -1 +1 @@ -!function(){"use strict";var e=function(t,i){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])},e(t,i)};function t(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=t}e(t,i),t.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var i=function(){return i=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!L(t,i))return!1}return!0},L=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&j(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return j(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},M=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||L(t,i)))},e}(),V=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},z=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},F=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return z(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),B="Event rejected due to exceeded retry count",$=function(e){return{promise:e||Promise.resolve()}},Q=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new F(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return $(this.dispatch(r))},e.prototype.identify=function(e,t){var i=V(e,t);return $(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return $(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new M;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return $(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return $(this.dispatch(n))},e.prototype.add=function(e){return this.config?$(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),$())},e.prototype.remove=function(e){return this.config?$(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),$())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(z(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,z(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=z(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return $(this.timeline.flush())},e}(),K=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return j(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),H="Amplitude Logger ",W=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=Y(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:return null===(s=o.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(s,e),[3,4]):("body"in s?this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(X(s))):this.fulfillRequest(e,s.statusCode,s.status),[2]);case 3:return a=o.sent(),u=(c=a)instanceof Error?c.message:String(c),this.config.loggerProvider.error(u),this.fulfillRequest(e,0,u),[3,4];case 4:return[2]}var c}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(X(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));d.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(z(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ne=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},re=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},oe=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},se=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=oe(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ae=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ue)},ce=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),le=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),de=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[D,t,e.substring(0,i)].filter(Boolean).join("_")},pe=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),fe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(le),ve="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},he={exports:{}};ee=he,te=he.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",q="Xiaomi",D="Zebra",A="Facebook",C=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},z=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?M(e,275):e,this},this.setUA(f),this};$.VERSION="0.7.31",$.BROWSER=C([a,l,"major"]),$.CPU=C([d]),$.DEVICE=C([s,c,u,p,f,h,v,g,b]),$.ENGINE=$.OS=C([a,l]),ee.exports&&(te=ee.exports=$),te.UAParser=$;var Q=typeof e!==n&&(e.jQuery||e.Zepto);if(Q&&!Q.ua){var K=new $;Q.ua=K.getResult(),Q.ua.get=function(){return K.getUA()},Q.ua.set=function(e){K.setUA(e);var t=K.getResult();for(var i in t)Q.ua[i]=t[i]}}}("object"==typeof window?window:ve);var ge=he.exports,be=function(){function e(){this.ua=new he.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:we(),platform:"Web",os:ye(this.ua),deviceModel:me(this.ua)}},e}(),ye=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},me=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},we=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},_e=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),Ie=function(){return Ie=Object.assign||function(e){for(var t,i=1,n=arguments.length;i=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(null!==(I=null!==(_=t.sessionId)&&void 0!==_?_:this.config.sessionId)&&void 0!==I?I:Date.now()),B=!0),($=Oe()).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new ie).promise];case 11:return W.sent(),[4,this.add(new qe).promise];case 12:return W.sent(),[4,this.add(new xe).promise];case 13:return W.sent(),("boolean"==typeof(Z=this.config.defaultTracking)?Z:null==Z?void 0:Z.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(st,((n={})[ct]=o,n[lt]=i.pathname,n[dt]=e.id,n[pt]=e.text,n[ft]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,15];case 14:W.sent(),W.label=15;case 15:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),t.track(ot,((r={})[vt]=e.id,r[ht]=e.name,r[gt]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,17];case 16:W.sent(),W.label=17;case 17:return(null===(k=this.config.attribution)||void 0===k?void 0:k.disabled)?[3,19]:(Q=function(){for(var e,t,n=this,s=[],u=0;uthis.config.sessionTimeout&&this.setSessionId(i),[2,e.prototype.process.call(this,t)]}))}))},n}(Q),mt=function(){var e=new yt;return{init:ae(e.init.bind(e),"init",re(e),se(e,["config"])),add:ae(e.add.bind(e),"add",re(e),se(e,["config.apiKey","timeline.plugins"])),remove:ae(e.remove.bind(e),"remove",re(e),se(e,["config.apiKey","timeline.plugins"])),track:ae(e.track.bind(e),"track",re(e),se(e,["config.apiKey","timeline.queue.length"])),logEvent:ae(e.logEvent.bind(e),"logEvent",re(e),se(e,["config.apiKey","timeline.queue.length"])),identify:ae(e.identify.bind(e),"identify",re(e),se(e,["config.apiKey","timeline.queue.length"])),groupIdentify:ae(e.groupIdentify.bind(e),"groupIdentify",re(e),se(e,["config.apiKey","timeline.queue.length"])),setGroup:ae(e.setGroup.bind(e),"setGroup",re(e),se(e,["config.apiKey","timeline.queue.length"])),revenue:ae(e.revenue.bind(e),"revenue",re(e),se(e,["config.apiKey","timeline.queue.length"])),flush:ae(e.flush.bind(e),"flush",re(e),se(e,["config.apiKey","timeline.queue.length"])),getUserId:ae(e.getUserId.bind(e),"getUserId",re(e),se(e,["config","config.userId"])),setUserId:ae(e.setUserId.bind(e),"setUserId",re(e),se(e,["config","config.userId"])),getDeviceId:ae(e.getDeviceId.bind(e),"getDeviceId",re(e),se(e,["config","config.deviceId"])),setDeviceId:ae(e.setDeviceId.bind(e),"setDeviceId",re(e),se(e,["config","config.deviceId"])),reset:ae(e.reset.bind(e),"reset",re(e),se(e,["config","config.userId","config.deviceId"])),getSessionId:ae(e.getSessionId.bind(e),"getSessionId",re(e),se(e,["config"])),setSessionId:ae(e.setSessionId.bind(e),"setSessionId",re(e),se(e,["config"])),extendSession:ae(e.extendSession.bind(e),"extendSession",re(e),se(e,["config"])),setOptOut:ae(e.setOptOut.bind(e),"setOptOut",re(e),se(e,["config"])),setTransport:ae(e.setTransport.bind(e),"setTransport",re(e),se(e,["config"]))}},wt=mt(),_t=wt.add,It=wt.extendSession,kt=wt.flush,Et=wt.getDeviceId,St=wt.getSessionId,Tt=wt.getUserId,Ot=wt.groupIdentify,xt=wt.identify,Pt=wt.init,Rt=wt.logEvent,Nt=wt.remove,Ut=wt.reset,qt=wt.revenue,Dt=wt.setDeviceId,At=wt.setGroup,Ct=wt.setOptOut,jt=wt.setSessionId,Lt=wt.setTransport,Mt=wt.setUserId,Vt=wt.track,zt=Object.freeze({__proto__:null,add:_t,extendSession:It,flush:kt,getDeviceId:Et,getSessionId:St,getUserId:Tt,groupIdentify:Ot,identify:xt,init:Pt,logEvent:Rt,remove:Nt,reset:Ut,revenue:qt,setDeviceId:Dt,setGroup:At,setOptOut:Ct,setSessionId:jt,setTransport:Lt,setUserId:Mt,track:Vt,Types:q,createInstance:mt,runQueuedFunctions:Re,Revenue:K,Identify:M});!function(){var e=b();if(e){var t=function(e){var t=mt(),i=b();return i&&i.amplitude&&i.amplitude._iq&&e&&(i.amplitude._iq[e]=t),t};if(e.amplitude=Object.assign(e.amplitude||{},zt,{createInstance:t}),e.amplitude.invoked){var i=e.amplitude._q;e.amplitude._q=[],Re(zt,i);for(var n=Object.keys(e.amplitude._iq)||[],r=0;r0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!L(t,i))return!1}return!0},L=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&j(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return j(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},M=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||L(t,i)))},e}(),V=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},z=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},F=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return z(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),B="Event rejected due to exceeded retry count",$=function(e){return{promise:e||Promise.resolve()}},Q=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new F(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return $(this.dispatch(r))},e.prototype.identify=function(e,t){var i=V(e,t);return $(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return $(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new M;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return $(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return $(this.dispatch(n))},e.prototype.add=function(e){return this.config?$(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),$())},e.prototype.remove=function(e){return this.config?$(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),$())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(z(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,z(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=z(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return $(this.timeline.flush())},e}(),K=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return j(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),H="Amplitude Logger ",W=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=Y(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:return null===(s=o.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(s,e),[3,4]):("body"in s?this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(X(s))):this.fulfillRequest(e,s.statusCode,s.status),[2]);case 3:return a=o.sent(),u=(c=a)instanceof Error?c.message:String(c),this.config.loggerProvider.error(u),this.fulfillRequest(e,0,u),[3,4];case 4:return[2]}var c}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(X(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));d.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(z(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ne=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},re=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},oe=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},se=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=oe(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ae=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ue)},ce=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),le=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),de=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[D,t,e.substring(0,i)].filter(Boolean).join("_")},pe=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),fe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(le),ve="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},he={exports:{}};ee=he,te=he.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",q="Xiaomi",D="Zebra",A="Facebook",C=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},z=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?M(e,275):e,this},this.setUA(f),this};$.VERSION="0.7.31",$.BROWSER=C([a,l,"major"]),$.CPU=C([d]),$.DEVICE=C([s,c,u,p,f,h,v,g,b]),$.ENGINE=$.OS=C([a,l]),ee.exports&&(te=ee.exports=$),te.UAParser=$;var Q=typeof e!==n&&(e.jQuery||e.Zepto);if(Q&&!Q.ua){var K=new $;Q.ua=K.getResult(),Q.ua.get=function(){return K.getUA()},Q.ua.set=function(e){K.setUA(e);var t=K.getResult();for(var i in t)Q.ua[i]=t[i]}}}("object"==typeof window?window:ve);var ge=he.exports,be=function(){function e(){this.ua=new he.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:we(),platform:"Web",os:ye(this.ua),deviceModel:me(this.ua)}},e}(),ye=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},me=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},we=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},_e=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),Ie=function(){return Ie=Object.assign||function(e){for(var t,i=1,n=arguments.length;i=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(null!==(I=null!==(_=t.sessionId)&&void 0!==_?_:this.config.sessionId)&&void 0!==I?I:Date.now()),B=!0),($=Oe(t.instanceName)).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new ie).promise];case 11:return W.sent(),[4,this.add(new qe).promise];case 12:return W.sent(),[4,this.add(new xe).promise];case 13:return W.sent(),("boolean"==typeof(Z=this.config.defaultTracking)?Z:null==Z?void 0:Z.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(st,((n={})[ct]=o,n[lt]=i.pathname,n[dt]=e.id,n[pt]=e.text,n[ft]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,15];case 14:W.sent(),W.label=15;case 15:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),t.track(ot,((r={})[vt]=e.id,r[ht]=e.name,r[gt]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,17];case 16:W.sent(),W.label=17;case 17:return(null===(k=this.config.attribution)||void 0===k?void 0:k.disabled)?[3,19]:(Q=function(){for(var e,t,n=this,s=[],u=0;uthis.config.sessionTimeout&&this.setSessionId(i),[2,e.prototype.process.call(this,t)]}))}))},n}(Q),mt=function(){var e=new yt;return{init:ae(e.init.bind(e),"init",re(e),se(e,["config"])),add:ae(e.add.bind(e),"add",re(e),se(e,["config.apiKey","timeline.plugins"])),remove:ae(e.remove.bind(e),"remove",re(e),se(e,["config.apiKey","timeline.plugins"])),track:ae(e.track.bind(e),"track",re(e),se(e,["config.apiKey","timeline.queue.length"])),logEvent:ae(e.logEvent.bind(e),"logEvent",re(e),se(e,["config.apiKey","timeline.queue.length"])),identify:ae(e.identify.bind(e),"identify",re(e),se(e,["config.apiKey","timeline.queue.length"])),groupIdentify:ae(e.groupIdentify.bind(e),"groupIdentify",re(e),se(e,["config.apiKey","timeline.queue.length"])),setGroup:ae(e.setGroup.bind(e),"setGroup",re(e),se(e,["config.apiKey","timeline.queue.length"])),revenue:ae(e.revenue.bind(e),"revenue",re(e),se(e,["config.apiKey","timeline.queue.length"])),flush:ae(e.flush.bind(e),"flush",re(e),se(e,["config.apiKey","timeline.queue.length"])),getUserId:ae(e.getUserId.bind(e),"getUserId",re(e),se(e,["config","config.userId"])),setUserId:ae(e.setUserId.bind(e),"setUserId",re(e),se(e,["config","config.userId"])),getDeviceId:ae(e.getDeviceId.bind(e),"getDeviceId",re(e),se(e,["config","config.deviceId"])),setDeviceId:ae(e.setDeviceId.bind(e),"setDeviceId",re(e),se(e,["config","config.deviceId"])),reset:ae(e.reset.bind(e),"reset",re(e),se(e,["config","config.userId","config.deviceId"])),getSessionId:ae(e.getSessionId.bind(e),"getSessionId",re(e),se(e,["config"])),setSessionId:ae(e.setSessionId.bind(e),"setSessionId",re(e),se(e,["config"])),extendSession:ae(e.extendSession.bind(e),"extendSession",re(e),se(e,["config"])),setOptOut:ae(e.setOptOut.bind(e),"setOptOut",re(e),se(e,["config"])),setTransport:ae(e.setTransport.bind(e),"setTransport",re(e),se(e,["config"]))}},wt=mt(),_t=wt.add,It=wt.extendSession,kt=wt.flush,Et=wt.getDeviceId,St=wt.getSessionId,Tt=wt.getUserId,Ot=wt.groupIdentify,xt=wt.identify,Pt=wt.init,Rt=wt.logEvent,Nt=wt.remove,Ut=wt.reset,qt=wt.revenue,Dt=wt.setDeviceId,At=wt.setGroup,Ct=wt.setOptOut,jt=wt.setSessionId,Lt=wt.setTransport,Mt=wt.setUserId,Vt=wt.track,zt=Object.freeze({__proto__:null,add:_t,extendSession:It,flush:kt,getDeviceId:Et,getSessionId:St,getUserId:Tt,groupIdentify:Ot,identify:xt,init:Pt,logEvent:Rt,remove:Nt,reset:Ut,revenue:qt,setDeviceId:Dt,setGroup:At,setOptOut:Ct,setSessionId:jt,setTransport:Lt,setUserId:Mt,track:Vt,Types:q,createInstance:mt,runQueuedFunctions:Re,Revenue:K,Identify:M});!function(){var e=b();if(e){var t=function(e){var t=mt(),i=b();return i&&i.amplitude&&i.amplitude._iq&&e&&(i.amplitude._iq[e]=t),t};if(e.amplitude=Object.assign(e.amplitude||{},zt,{createInstance:t}),e.amplitude.invoked){var i=e.amplitude._q;e.amplitude._q=[],Re(zt,i);for(var n=Object.keys(e.amplitude._iq)||[],r=0;r ```html diff --git a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js index 8be97ab3d..4a216eefc 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-GCneBnzsMTw+kmn6TwyFynia1fmqj+38TkwyYEyW8AkwF+Qw8IAh1CjqSgDYZ9X+'; + as.integrity = 'sha384-uQFYsRWKr27i5VRoSShdW+SWnMC3RYcwlKIyk4cs8E1KrVgcdqeAI4Dca3cGejM+'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.5-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.6-min.js.gz'; as.onload = function () { if (!window.amplitudeGTM.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/generated/amplitude-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-snippet.js index d4784919c..cac52bf19 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-ocQG0VTdcJGeNGgEnHLUASJ+2qacfMa5iU5EsLRXLdGs/g2XAPDUI4zdjSMlhO5U'; + as.integrity = 'sha384-LxzsESYOYIQDLuCbztiSAfn/WnK7IQMVua4D9OvBkNgfqKHs8lZOfldylsVDG7m4'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.5-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.6-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/package.json b/packages/marketing-analytics-browser/package.json index 4d151e352..070ca4b1c 100644 --- a/packages/marketing-analytics-browser/package.json +++ b/packages/marketing-analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/marketing-analytics-browser", - "version": "1.0.5", + "version": "1.0.6", "description": "Official Amplitude SDK for Web and Marketing Analytics", "keywords": [ "analytics", @@ -43,12 +43,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.11.2", - "@amplitude/analytics-client-common": "^1.0.3", - "@amplitude/analytics-core": "^1.1.2", - "@amplitude/analytics-types": "^1.2.1", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.5", - "@amplitude/plugin-web-attribution-browser": "^1.0.5", + "@amplitude/analytics-browser": "^1.12.0", + "@amplitude/analytics-client-common": "^1.1.0", + "@amplitude/analytics-core": "^1.2.0", + "@amplitude/analytics-types": "^1.3.0", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.6", + "@amplitude/plugin-web-attribution-browser": "^1.0.6", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/marketing-analytics-browser/src/version.ts b/packages/marketing-analytics-browser/src/version.ts index 6cec2cb92..3f6f127aa 100644 --- a/packages/marketing-analytics-browser/src/version.ts +++ b/packages/marketing-analytics-browser/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.0.5'; +export const VERSION = '1.0.6'; diff --git a/packages/plugin-page-view-tracking-browser/CHANGELOG.md b/packages/plugin-page-view-tracking-browser/CHANGELOG.md index 48be54e1c..401e9f308 100644 --- a/packages/plugin-page-view-tracking-browser/CHANGELOG.md +++ b/packages/plugin-page-view-tracking-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.5...@amplitude/plugin-page-view-tracking-browser@1.0.6) (2023-06-26) + +**Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.4...@amplitude/plugin-page-view-tracking-browser@1.0.5) (2023-06-14) **Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser diff --git a/packages/plugin-page-view-tracking-browser/package.json b/packages/plugin-page-view-tracking-browser/package.json index d77195f30..a2cd4b9a0 100644 --- a/packages/plugin-page-view-tracking-browser/package.json +++ b/packages/plugin-page-view-tracking-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-page-view-tracking-browser", - "version": "1.0.5", + "version": "1.0.6", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -37,12 +37,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.3", - "@amplitude/analytics-types": "^1.2.1", + "@amplitude/analytics-client-common": "^1.1.0", + "@amplitude/analytics-types": "^1.3.0", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.11.2", + "@amplitude/analytics-browser": "^1.12.0", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", diff --git a/packages/plugin-web-attribution-browser/CHANGELOG.md b/packages/plugin-web-attribution-browser/CHANGELOG.md index 909d34853..a1bb02e33 100644 --- a/packages/plugin-web-attribution-browser/CHANGELOG.md +++ b/packages/plugin-web-attribution-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.5...@amplitude/plugin-web-attribution-browser@1.0.6) (2023-06-26) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.4...@amplitude/plugin-web-attribution-browser@1.0.5) (2023-06-14) **Note:** Version bump only for package @amplitude/plugin-web-attribution-browser diff --git a/packages/plugin-web-attribution-browser/package.json b/packages/plugin-web-attribution-browser/package.json index 3a1667300..69a77c34d 100644 --- a/packages/plugin-web-attribution-browser/package.json +++ b/packages/plugin-web-attribution-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-web-attribution-browser", - "version": "1.0.5", + "version": "1.0.6", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -37,12 +37,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.0.3", - "@amplitude/analytics-types": "^1.2.1", + "@amplitude/analytics-client-common": "^1.1.0", + "@amplitude/analytics-types": "^1.3.0", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.11.2", + "@amplitude/analytics-browser": "^1.12.0", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", From 07fc403ed2e08dc6685de1d150f0518abd859e09 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Tue, 27 Jun 2023 09:52:29 -0400 Subject: [PATCH 039/214] feat(plugins): update name of property for session replay --- packages/plugin-session-replay/src/constants.ts | 5 +++++ packages/plugin-session-replay/src/session-replay.ts | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 packages/plugin-session-replay/src/constants.ts diff --git a/packages/plugin-session-replay/src/constants.ts b/packages/plugin-session-replay/src/constants.ts new file mode 100644 index 000000000..2ca6feab9 --- /dev/null +++ b/packages/plugin-session-replay/src/constants.ts @@ -0,0 +1,5 @@ +export const DEFAULT_EVENT_PROPERTY_PREFIX = '[Amplitude]'; + +export const DEFAULT_SESSION_REPLAY_PROPERTY = `${DEFAULT_EVENT_PROPERTY_PREFIX} Session Recorded`; +export const DEFAULT_SESSION_START_EVENT = 'session_start'; +export const DEFAULT_SESSION_END_EVENT = 'session_end'; diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay/src/session-replay.ts index e70209910..a49895506 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay/src/session-replay.ts @@ -2,6 +2,7 @@ import { AMPLITUDE_PREFIX, BaseTransport } from '@amplitude/analytics-core'; import { BrowserConfig, EnrichmentPlugin, Event, PluginType, Status } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import { pack, record } from 'rrweb'; +import { DEFAULT_SESSION_END_EVENT, DEFAULT_SESSION_REPLAY_PROPERTY, DEFAULT_SESSION_START_EVENT } from './constants'; import { shouldSplitEventsList } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from './messages'; import { Events, IDBStore, SessionReplayContext } from './typings/session-replay'; @@ -38,12 +39,12 @@ export class SessionReplayPlugin implements EnrichmentPlugin { execute(event: Event) { event.event_properties = { ...event.event_properties, - session_replay_enabled: true, + [DEFAULT_SESSION_REPLAY_PROPERTY]: true, }; - if (event.event_type === 'session_start' && this.stopRecordingEvents) { + if (event.event_type === DEFAULT_SESSION_START_EVENT && this.stopRecordingEvents) { this.stopRecordingEvents(); this.recordEvents(); - } else if (event.event_type === 'session_end') { + } else if (event.event_type === DEFAULT_SESSION_END_EVENT) { if (event.session_id) { this.sendEventsList({ events: this.events, From 2c76247f8b994a52650db26dca3603319d13857e Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Tue, 27 Jun 2023 14:22:32 -0400 Subject: [PATCH 040/214] feat(plugins): change file names --- packages/analytics-core/src/core-client.ts | 16 ++++++----- .../README.md | 10 +++---- .../jest.config.js | 0 .../package.json | 0 .../rollup.config.js | 0 .../src/constants.ts | 0 .../src/helpers.ts | 0 .../src/index.ts | 0 .../src/messages.ts | 0 .../src/session-replay.ts | 3 +- .../src/typings/session-replay.ts | 28 +++++++++++++++++++ .../test/session-replay.test.ts | 0 .../tsconfig.es5.json | 0 .../tsconfig.esm.json | 0 .../tsconfig.json | 0 .../src/typings/session-replay.ts | 16 ----------- 16 files changed, 43 insertions(+), 30 deletions(-) rename packages/{plugin-session-replay => plugin-session-replay-browser}/README.md (84%) rename packages/{plugin-session-replay => plugin-session-replay-browser}/jest.config.js (100%) rename packages/{plugin-session-replay => plugin-session-replay-browser}/package.json (100%) rename packages/{plugin-session-replay => plugin-session-replay-browser}/rollup.config.js (100%) rename packages/{plugin-session-replay => plugin-session-replay-browser}/src/constants.ts (100%) rename packages/{plugin-session-replay => plugin-session-replay-browser}/src/helpers.ts (100%) rename packages/{plugin-session-replay => plugin-session-replay-browser}/src/index.ts (100%) rename packages/{plugin-session-replay => plugin-session-replay-browser}/src/messages.ts (100%) rename packages/{plugin-session-replay => plugin-session-replay-browser}/src/session-replay.ts (99%) create mode 100644 packages/plugin-session-replay-browser/src/typings/session-replay.ts rename packages/{plugin-session-replay => plugin-session-replay-browser}/test/session-replay.test.ts (100%) rename packages/{plugin-session-replay => plugin-session-replay-browser}/tsconfig.es5.json (100%) rename packages/{plugin-session-replay => plugin-session-replay-browser}/tsconfig.esm.json (100%) rename packages/{plugin-session-replay => plugin-session-replay-browser}/tsconfig.json (100%) delete mode 100644 packages/plugin-session-replay/src/typings/session-replay.ts diff --git a/packages/analytics-core/src/core-client.ts b/packages/analytics-core/src/core-client.ts index 6d2c84f93..9ef6a1ccc 100644 --- a/packages/analytics-core/src/core-client.ts +++ b/packages/analytics-core/src/core-client.ts @@ -1,24 +1,24 @@ import { - CoreClient, + BaseEvent, Config, + CoreClient, Event, - BaseEvent, EventOptions, Identify, Plugin, - Revenue, Result, + Revenue, } from '@amplitude/analytics-types'; +import { CLIENT_NOT_INITIALIZED, OPT_OUT_MESSAGE } from './messages'; +import { Timeline } from './timeline'; import { + createGroupEvent, createGroupIdentifyEvent, createIdentifyEvent, - createTrackEvent, createRevenueEvent, - createGroupEvent, + createTrackEvent, } from './utils/event-builder'; -import { Timeline } from './timeline'; import { buildResult } from './utils/result-builder'; -import { CLIENT_NOT_INITIALIZED, OPT_OUT_MESSAGE } from './messages'; import { returnWrapper } from './utils/return-wrapper'; export class AmplitudeCore implements CoreClient { @@ -53,6 +53,7 @@ export class AmplitudeCore implements CoreClient { } track(eventInput: BaseEvent | string, eventProperties?: Record, eventOptions?: EventOptions) { + console.log('track', eventInput); const event = createTrackEvent(eventInput, eventProperties, eventOptions); return returnWrapper(this.dispatch(event)); } @@ -80,6 +81,7 @@ export class AmplitudeCore implements CoreClient { } add(plugin: Plugin) { + console.log('adding plugin'); if (!this.config) { this.q.push(this.add.bind(this, plugin)); return returnWrapper(); diff --git a/packages/plugin-session-replay/README.md b/packages/plugin-session-replay-browser/README.md similarity index 84% rename from packages/plugin-session-replay/README.md rename to packages/plugin-session-replay-browser/README.md index a1ed2a77e..5d06a97c9 100644 --- a/packages/plugin-session-replay/README.md +++ b/packages/plugin-session-replay-browser/README.md @@ -5,7 +5,7 @@

-# @amplitude/plugin-session-replay +# @amplitude/plugin-session-replay-browser Official Browser SDK plugin for session replay @@ -15,10 +15,10 @@ This package is published on NPM registry and is available to be installed using ```sh # npm -npm install @amplitude/plugin-session-replay +npm install @amplitude/plugin-session-replay-browser # yarn -yarn add @amplitude/plugin-session-replay +yarn add @amplitude/plugin-session-replay-browser ``` ## Usage @@ -40,7 +40,7 @@ import { SessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'; The plugin must be registered with the amplitude instance via the following code: ```typescript -amplitude.init(API_KEY) +amplitude.init(API_KEY); const sessionReplayTracking = new SessionReplayPlugin(); -amplitude.add(sessionReplayTracking) +amplitude.add(sessionReplayTracking); ``` \ No newline at end of file diff --git a/packages/plugin-session-replay/jest.config.js b/packages/plugin-session-replay-browser/jest.config.js similarity index 100% rename from packages/plugin-session-replay/jest.config.js rename to packages/plugin-session-replay-browser/jest.config.js diff --git a/packages/plugin-session-replay/package.json b/packages/plugin-session-replay-browser/package.json similarity index 100% rename from packages/plugin-session-replay/package.json rename to packages/plugin-session-replay-browser/package.json diff --git a/packages/plugin-session-replay/rollup.config.js b/packages/plugin-session-replay-browser/rollup.config.js similarity index 100% rename from packages/plugin-session-replay/rollup.config.js rename to packages/plugin-session-replay-browser/rollup.config.js diff --git a/packages/plugin-session-replay/src/constants.ts b/packages/plugin-session-replay-browser/src/constants.ts similarity index 100% rename from packages/plugin-session-replay/src/constants.ts rename to packages/plugin-session-replay-browser/src/constants.ts diff --git a/packages/plugin-session-replay/src/helpers.ts b/packages/plugin-session-replay-browser/src/helpers.ts similarity index 100% rename from packages/plugin-session-replay/src/helpers.ts rename to packages/plugin-session-replay-browser/src/helpers.ts diff --git a/packages/plugin-session-replay/src/index.ts b/packages/plugin-session-replay-browser/src/index.ts similarity index 100% rename from packages/plugin-session-replay/src/index.ts rename to packages/plugin-session-replay-browser/src/index.ts diff --git a/packages/plugin-session-replay/src/messages.ts b/packages/plugin-session-replay-browser/src/messages.ts similarity index 100% rename from packages/plugin-session-replay/src/messages.ts rename to packages/plugin-session-replay-browser/src/messages.ts diff --git a/packages/plugin-session-replay/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts similarity index 99% rename from packages/plugin-session-replay/src/session-replay.ts rename to packages/plugin-session-replay-browser/src/session-replay.ts index a49895506..da6aa2e0e 100644 --- a/packages/plugin-session-replay/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -13,7 +13,7 @@ const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON s const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; export class SessionReplayPlugin implements EnrichmentPlugin { - name = '@amplitude/plugin-session-replay'; + name = '@amplitude/plugin-session-replay-browser'; type = PluginType.ENRICHMENT as const; // this.config is defined in setup() which will always be called first // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -60,7 +60,6 @@ export class SessionReplayPlugin implements EnrichmentPlugin { async emptyStoreAndReset() { const storedReplaySessions = await this.getAllSessionEventsFromStore(); - console.log('in empty store after await'); if (storedReplaySessions) { for (const sessionId in storedReplaySessions) { const storedReplayEvents = storedReplaySessions[sessionId]; diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts new file mode 100644 index 000000000..2ebd2612b --- /dev/null +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -0,0 +1,28 @@ +import { BrowserClient, EnrichmentPlugin } from '@amplitude/analytics-types'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface Options {} + +export interface SessionReplayPlugin { + (client: BrowserClient, options?: Options): EnrichmentPlugin; + (options?: Options): EnrichmentPlugin; +} + +export type SessionReplayPluginParameters = [BrowserClient, Options?] | [Options?]; + +export type Events = string[]; + +export interface SessionReplayContext { + events: Events; + sequenceId: number; + attempts: number; + timeout: number; + sessionId: number; +} + +export interface IDBStore { + [sessionId: number]: { + events: Events; + sequenceId: number; + }; +} diff --git a/packages/plugin-session-replay/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts similarity index 100% rename from packages/plugin-session-replay/test/session-replay.test.ts rename to packages/plugin-session-replay-browser/test/session-replay.test.ts diff --git a/packages/plugin-session-replay/tsconfig.es5.json b/packages/plugin-session-replay-browser/tsconfig.es5.json similarity index 100% rename from packages/plugin-session-replay/tsconfig.es5.json rename to packages/plugin-session-replay-browser/tsconfig.es5.json diff --git a/packages/plugin-session-replay/tsconfig.esm.json b/packages/plugin-session-replay-browser/tsconfig.esm.json similarity index 100% rename from packages/plugin-session-replay/tsconfig.esm.json rename to packages/plugin-session-replay-browser/tsconfig.esm.json diff --git a/packages/plugin-session-replay/tsconfig.json b/packages/plugin-session-replay-browser/tsconfig.json similarity index 100% rename from packages/plugin-session-replay/tsconfig.json rename to packages/plugin-session-replay-browser/tsconfig.json diff --git a/packages/plugin-session-replay/src/typings/session-replay.ts b/packages/plugin-session-replay/src/typings/session-replay.ts deleted file mode 100644 index 21445cbb5..000000000 --- a/packages/plugin-session-replay/src/typings/session-replay.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type Events = string[]; - -export interface SessionReplayContext { - events: Events; - sequenceId: number; - attempts: number; - timeout: number; - sessionId: number; -} - -export interface IDBStore { - [sessionId: number]: { - events: Events; - sequenceId: number; - }; -} From 61185acc3504b96be9ea8144fc62967937235b37 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Tue, 27 Jun 2023 14:56:25 -0400 Subject: [PATCH 041/214] feat(plugins): expose function as external interface --- .../plugin-session-replay-browser/README.md | 4 +- .../src/index.ts | 2 +- .../src/session-replay.ts | 18 +++-- .../src/typings/session-replay.ts | 56 ++++++++++++--- .../test/session-replay.test.ts | 68 +++++++++---------- 5 files changed, 99 insertions(+), 49 deletions(-) diff --git a/packages/plugin-session-replay-browser/README.md b/packages/plugin-session-replay-browser/README.md index 5d06a97c9..5f546afe4 100644 --- a/packages/plugin-session-replay-browser/README.md +++ b/packages/plugin-session-replay-browser/README.md @@ -32,7 +32,7 @@ This plugin works on top of Amplitude Browser SDK and adds session replay featur ```typescript import * as amplitude from '@amplitude/analytics-browser'; -import { SessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'; +import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'; ``` ### 2. Instantiate session replay plugin and install plugin to Amplitude SDK @@ -41,6 +41,6 @@ The plugin must be registered with the amplitude instance via the following code ```typescript amplitude.init(API_KEY); -const sessionReplayTracking = new SessionReplayPlugin(); +const sessionReplayTracking = sessionReplayPlugin(); amplitude.add(sessionReplayTracking); ``` \ No newline at end of file diff --git a/packages/plugin-session-replay-browser/src/index.ts b/packages/plugin-session-replay-browser/src/index.ts index 7ec717f3b..b84fa2fa0 100644 --- a/packages/plugin-session-replay-browser/src/index.ts +++ b/packages/plugin-session-replay-browser/src/index.ts @@ -1 +1 @@ -export { SessionReplayPlugin as Plugin, SessionReplayPlugin } from './session-replay'; +export { sessionReplayPlugin as plugin, sessionReplayPlugin } from './session-replay'; diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index da6aa2e0e..d4d1926c7 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -1,18 +1,24 @@ import { AMPLITUDE_PREFIX, BaseTransport } from '@amplitude/analytics-core'; -import { BrowserConfig, EnrichmentPlugin, Event, PluginType, Status } from '@amplitude/analytics-types'; +import { BrowserConfig, Event, PluginType, Status } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import { pack, record } from 'rrweb'; import { DEFAULT_SESSION_END_EVENT, DEFAULT_SESSION_REPLAY_PROPERTY, DEFAULT_SESSION_START_EVENT } from './constants'; import { shouldSplitEventsList } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from './messages'; -import { Events, IDBStore, SessionReplayContext } from './typings/session-replay'; +import { + Events, + IDBStore, + SessionReplayContext, + SessionReplayEnrichmentPlugin, + SessionReplayPlugin, +} from './typings/session-replay'; const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/track'; const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; -export class SessionReplayPlugin implements EnrichmentPlugin { +class SessionReplay implements SessionReplayEnrichmentPlugin { name = '@amplitude/plugin-session-replay-browser'; type = PluginType.ENRICHMENT as const; // this.config is defined in setup() which will always be called first @@ -36,7 +42,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { void this.emptyStoreAndReset(); } - execute(event: Event) { + async execute(event: Event) { event.event_properties = { ...event.event_properties, [DEFAULT_SESSION_REPLAY_PROPERTY]: true, @@ -284,3 +290,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { } } } + +export const sessionReplayPlugin: SessionReplayPlugin = () => { + return new SessionReplay(); +}; diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index 2ebd2612b..f0bd54a48 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -1,15 +1,9 @@ -import { BrowserClient, EnrichmentPlugin } from '@amplitude/analytics-types'; +import { BrowserClient, BrowserConfig, EnrichmentPlugin } from '@amplitude/analytics-types'; +import { record } from 'rrweb'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface Options {} -export interface SessionReplayPlugin { - (client: BrowserClient, options?: Options): EnrichmentPlugin; - (options?: Options): EnrichmentPlugin; -} - -export type SessionReplayPluginParameters = [BrowserClient, Options?] | [Options?]; - export type Events = string[]; export interface SessionReplayContext { @@ -26,3 +20,49 @@ export interface IDBStore { sequenceId: number; }; } +export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { + config: BrowserConfig; + storageKey: string; + retryTimeout: number; + events: Events; + currentSequenceId: number; + queue: SessionReplayContext[]; + stopRecordingEvents: ReturnType | null; + maxPersistedEventsSize: number; + emptyStoreAndReset: () => Promise; + recordEvents: () => void; + sendEventsList: ({ + events, + sequenceId, + sessionId, + }: { + events: string[]; + sequenceId: number; + sessionId: number; + }) => void; + addToQueue: (...list: SessionReplayContext[]) => void; + schedule: (timeout: number) => void; + flush: (useRetry?: boolean) => Promise; + send: (context: SessionReplayContext, useRetry?: boolean) => Promise; + completeRequest({ + context, + err, + success, + removeEvents, + }: { + context: SessionReplayContext; + err?: string | undefined; + success?: string | undefined; + removeEvents?: boolean | undefined; + }): void; + getAllSessionEventsFromStore: () => Promise; + storeEventsForSession: (events: Events, sequenceId: number) => Promise; + removeSessionEventsStore: (sessionId: number) => Promise; +} + +export interface SessionReplayPlugin { + (client: BrowserClient, options?: Options): SessionReplayEnrichmentPlugin; + (options?: Options): SessionReplayEnrichmentPlugin; +} + +export type SessionReplayPluginParameters = [BrowserClient, Options?] | [Options?]; diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 195f980dc..0c1f3d4d4 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -5,7 +5,7 @@ import * as IDBKeyVal from 'idb-keyval'; import * as RRWeb from 'rrweb'; import { shouldSplitEventsList } from '../src/helpers'; import { SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from '../src/messages'; -import { SessionReplayPlugin } from '../src/session-replay'; +import { sessionReplayPlugin } from '../src/session-replay'; jest.mock('idb-keyval'); type MockedIDBKeyVal = jest.Mocked; @@ -96,7 +96,7 @@ describe('SessionReplayPlugin', () => { }); describe('setup', () => { test('should setup plugin', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); await sessionReplay.setup(mockConfig); expect(sessionReplay.config.transportProvider).toBeDefined(); expect(sessionReplay.config.serverUrl).toBe('url'); @@ -107,7 +107,7 @@ describe('SessionReplayPlugin', () => { }); test('should read events from storage and send them, then reset storage for session', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, sessionId: 456, @@ -156,7 +156,7 @@ describe('SessionReplayPlugin', () => { }); }); test('should handle no stored events', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, }; @@ -176,7 +176,7 @@ describe('SessionReplayPlugin', () => { test('should record events', async () => { const mockGetResolution = Promise.resolve({}); get.mockReturnValueOnce(mockGetResolution); - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); await sessionReplay.setup(mockConfig); await mockGetResolution; jest.runAllTimers(); @@ -185,8 +185,8 @@ describe('SessionReplayPlugin', () => { }); describe('execute', () => { - test('should add event property for session_replay_enabled', async () => { - const sessionReplay = new SessionReplayPlugin(); + test('should add event property for [Amplitude] Session Recorded', async () => { + const sessionReplay = sessionReplayPlugin(); await sessionReplay.setup(mockConfig); const event = { event_type: 'event_type', @@ -197,15 +197,15 @@ describe('SessionReplayPlugin', () => { }; const enrichedEvent = await sessionReplay.execute(event); - expect(enrichedEvent.event_properties).toEqual({ + expect(enrichedEvent?.event_properties).toEqual({ property_a: true, property_b: 123, - session_replay_enabled: true, + '[Amplitude] Session Recorded': true, }); }); test('should restart recording events when session_start fires', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); const mockStopRecordingEvents = jest.fn(); record.mockReturnValue(mockStopRecordingEvents); const mockGetResolution = Promise.resolve({}); @@ -222,7 +222,7 @@ describe('SessionReplayPlugin', () => { }); test('should send the current events list when session_end fires', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); @@ -248,7 +248,7 @@ describe('SessionReplayPlugin', () => { describe('recordEvents', () => { test('should store events in class and in IDB', () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; sessionReplay.recordEvents(); expect(sessionReplay.events).toEqual([]); @@ -265,7 +265,7 @@ describe('SessionReplayPlugin', () => { }); test('should split the events list and send', () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; sessionReplay.maxPersistedEventsSize = 20; const events = ['#'.repeat(20)]; @@ -297,7 +297,7 @@ describe('SessionReplayPlugin', () => { describe('addToQueue', () => { test('should add to queue and schedule a flush', () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; const schedule = jest.spyOn(sessionReplay, 'schedule').mockReturnValueOnce(undefined); const context = { @@ -313,7 +313,7 @@ describe('SessionReplayPlugin', () => { }); test('should not add to queue if attemps are greater than allowed retries', () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); sessionReplay.config = { ...mockConfig, flushMaxRetries: 1, @@ -343,7 +343,7 @@ describe('SessionReplayPlugin', () => { describe('schedule', () => { test('should schedule a flush', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (sessionReplay as any).scheduled = null; sessionReplay.queue = [ @@ -369,7 +369,7 @@ describe('SessionReplayPlugin', () => { }); test('should not schedule if one is already in progress', () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (sessionReplay as any).scheduled = setTimeout(jest.fn, 0); const flush = jest.spyOn(sessionReplay, 'flush').mockReturnValueOnce(Promise.resolve(undefined)); @@ -380,7 +380,7 @@ describe('SessionReplayPlugin', () => { describe('flush', () => { test('should call send', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; sessionReplay.queue = [ { @@ -399,7 +399,7 @@ describe('SessionReplayPlugin', () => { }); test('should send later', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; sessionReplay.queue = [ { @@ -428,7 +428,7 @@ describe('SessionReplayPlugin', () => { describe('send', () => { test('should make a request correctly', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; const context = { events: [mockEventString], @@ -452,7 +452,7 @@ describe('SessionReplayPlugin', () => { }); }); test('should remove session events from IDB store upon success', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; const context = { events: [mockEventString], @@ -483,7 +483,7 @@ describe('SessionReplayPlugin', () => { }); }); test('should not remove session events from IDB store upon failure', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; const context = { events: [mockEventString], @@ -501,7 +501,7 @@ describe('SessionReplayPlugin', () => { }); test('should retry if retry param is true', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; const context = { events: [mockEventString], @@ -534,7 +534,7 @@ describe('SessionReplayPlugin', () => { }); test('should not retry if retry param is false', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; const context = { events: [mockEventString], @@ -557,7 +557,7 @@ describe('SessionReplayPlugin', () => { describe('module level integration', () => { test('should handle unexpected error', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, loggerProvider: mockLoggerProvider, @@ -586,7 +586,7 @@ describe('SessionReplayPlugin', () => { status: 200, }); }); - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); sessionReplay.retryTimeout = 10; const config = { ...mockConfig, @@ -619,7 +619,7 @@ describe('SessionReplayPlugin', () => { status: 200, }); }); - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, flushMaxRetries: 2, @@ -645,7 +645,7 @@ describe('SessionReplayPlugin', () => { status: 200, }); }); - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, flushMaxRetries: 2, @@ -671,7 +671,7 @@ describe('SessionReplayPlugin', () => { status: 200, }); }); - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, flushMaxRetries: 2, @@ -689,7 +689,7 @@ describe('SessionReplayPlugin', () => { (fetch as jest.Mock).mockImplementationOnce(() => { return Promise.resolve(null); }); - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, flushMaxRetries: 2, @@ -711,7 +711,7 @@ describe('SessionReplayPlugin', () => { describe('idb error handling', () => { test('getAllSessionEventsFromStore should catch errors', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, loggerProvider: mockLoggerProvider, @@ -726,7 +726,7 @@ describe('SessionReplayPlugin', () => { ); }); test('storeEventsForSession should catch errors', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, loggerProvider: mockLoggerProvider, @@ -741,7 +741,7 @@ describe('SessionReplayPlugin', () => { ); }); test('removeSessionEventsStore should catch errors', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, loggerProvider: mockLoggerProvider, @@ -756,7 +756,7 @@ describe('SessionReplayPlugin', () => { ); }); test('removeSessionEventsStore should handle an undefined store', async () => { - const sessionReplay = new SessionReplayPlugin(); + const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, loggerProvider: mockLoggerProvider, From 4ace87ddc1fbdfaa0d2a33db939ef04e93ef0f70 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Tue, 27 Jun 2023 15:33:08 -0400 Subject: [PATCH 042/214] feat(plugins): remove mistake addition --- packages/analytics-core/src/core-client.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/analytics-core/src/core-client.ts b/packages/analytics-core/src/core-client.ts index 9ef6a1ccc..6d2c84f93 100644 --- a/packages/analytics-core/src/core-client.ts +++ b/packages/analytics-core/src/core-client.ts @@ -1,24 +1,24 @@ import { - BaseEvent, - Config, CoreClient, + Config, Event, + BaseEvent, EventOptions, Identify, Plugin, - Result, Revenue, + Result, } from '@amplitude/analytics-types'; -import { CLIENT_NOT_INITIALIZED, OPT_OUT_MESSAGE } from './messages'; -import { Timeline } from './timeline'; import { - createGroupEvent, createGroupIdentifyEvent, createIdentifyEvent, - createRevenueEvent, createTrackEvent, + createRevenueEvent, + createGroupEvent, } from './utils/event-builder'; +import { Timeline } from './timeline'; import { buildResult } from './utils/result-builder'; +import { CLIENT_NOT_INITIALIZED, OPT_OUT_MESSAGE } from './messages'; import { returnWrapper } from './utils/return-wrapper'; export class AmplitudeCore implements CoreClient { @@ -53,7 +53,6 @@ export class AmplitudeCore implements CoreClient { } track(eventInput: BaseEvent | string, eventProperties?: Record, eventOptions?: EventOptions) { - console.log('track', eventInput); const event = createTrackEvent(eventInput, eventProperties, eventOptions); return returnWrapper(this.dispatch(event)); } @@ -81,7 +80,6 @@ export class AmplitudeCore implements CoreClient { } add(plugin: Plugin) { - console.log('adding plugin'); if (!this.config) { this.q.push(this.add.bind(this, plugin)); return returnWrapper(); From b782eb36cf8c34154139411880967f68609da90f Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Wed, 28 Jun 2023 15:11:46 +0000 Subject: [PATCH 043/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.1.0 --- packages/plugin-session-replay-browser/CHANGELOG.md | 13 +++++++++++++ packages/plugin-session-replay-browser/package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 packages/plugin-session-replay-browser/CHANGELOG.md diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md new file mode 100644 index 000000000..758270f9e --- /dev/null +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -0,0 +1,13 @@ +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.1.0 (2023-06-28) + +### Features + +- **plugins:** change file names + ([2c76247](https://github.com/amplitude/Amplitude-TypeScript/commit/2c76247f8b994a52650db26dca3603319d13857e)) +- **plugins:** expose function as external interface + ([61185ac](https://github.com/amplitude/Amplitude-TypeScript/commit/61185acc3504b96be9ea8144fc62967937235b37)) diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index eff7229dd..d2635ef02 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.0.0", + "version": "0.1.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 902aa39c8dcccfdc48f44312fe11a811a39cba10 Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Thu, 29 Jun 2023 17:01:23 -0700 Subject: [PATCH 044/214] fix: missing core dependency for web attribution (#462) --- packages/plugin-web-attribution-browser/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/plugin-web-attribution-browser/package.json b/packages/plugin-web-attribution-browser/package.json index 69a77c34d..c2d1c0d51 100644 --- a/packages/plugin-web-attribution-browser/package.json +++ b/packages/plugin-web-attribution-browser/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@amplitude/analytics-client-common": "^1.1.0", + "@amplitude/analytics-core": "^1.2.0", "@amplitude/analytics-types": "^1.3.0", "tslib": "^2.4.1" }, From 1394ddb3775c7e5b1a3da87e6a4c973a586c6382 Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Thu, 29 Jun 2023 17:48:40 -0700 Subject: [PATCH 045/214] fix: allow plugins to teardown to remove listeners (#463) --- .../src/plugins/file-download-tracking.ts | 38 +++++++++- .../src/plugins/form-interaction-tracking.ts | 62 +++++++++++---- .../plugins/file-download-tracking.test.ts | 38 +++++++++- .../plugins/form-interaction-tracking.test.ts | 76 +++++++++++++++---- packages/analytics-core/src/timeline.ts | 13 ++-- packages/analytics-core/test/timeline.test.ts | 48 ++++++++++++ packages/analytics-types/src/plugin.ts | 3 + .../src/page-view-tracking.ts | 23 +++++- .../test/page-view-tracking.test.ts | 28 +++++++ 9 files changed, 286 insertions(+), 43 deletions(-) diff --git a/packages/analytics-browser/src/plugins/file-download-tracking.ts b/packages/analytics-browser/src/plugins/file-download-tracking.ts index 7e6087a08..faf9ad722 100644 --- a/packages/analytics-browser/src/plugins/file-download-tracking.ts +++ b/packages/analytics-browser/src/plugins/file-download-tracking.ts @@ -2,7 +2,31 @@ import { BrowserClient, PluginType, Event, EnrichmentPlugin } from '@amplitude/a import { DEFAULT_FILE_DOWNLOAD_EVENT, FILE_EXTENSION, FILE_NAME, LINK_ID, LINK_TEXT, LINK_URL } from '../constants'; import { BrowserConfig } from '../config'; +interface EventListener { + element: Element; + type: 'click'; + handler: () => void; +} + export const fileDownloadTracking = (): EnrichmentPlugin => { + let observer: MutationObserver | undefined; + let eventListeners: EventListener[] = []; + const addEventListener = (element: Element, type: 'click', handler: () => void) => { + element.addEventListener(type, handler); + eventListeners.push({ + element, + type, + handler, + }); + }; + const removeClickListeners = () => { + eventListeners.forEach(({ element, type, handler }) => { + /* istanbul ignore next */ + element?.removeEventListener(type, handler); + }); + eventListeners = []; + }; + const name = '@amplitude/plugin-file-download-tracking-browser'; const type = PluginType.ENRICHMENT; const setup = async (config: BrowserConfig, amplitude?: BrowserClient) => { @@ -15,6 +39,11 @@ export const fileDownloadTracking = (): EnrichmentPlugin => { return; } + /* istanbul ignore if */ + if (typeof document === 'undefined') { + return; + } + const addFileDownloadListener = (a: HTMLAnchorElement) => { let url: URL; try { @@ -28,7 +57,7 @@ export const fileDownloadTracking = (): EnrichmentPlugin => { const fileExtension = result?.[1]; if (fileExtension) { - a.addEventListener('click', () => { + addEventListener(a, 'click', () => { if (fileExtension) { amplitude.track(DEFAULT_FILE_DOWNLOAD_EVENT, { [FILE_EXTENSION]: fileExtension, @@ -52,7 +81,7 @@ export const fileDownloadTracking = (): EnrichmentPlugin => { // Adds listener to anchor tags added after initial load /* istanbul ignore else */ if (typeof MutationObserver !== 'undefined') { - const observer = new MutationObserver((mutations) => { + observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeName === 'A') { @@ -72,11 +101,16 @@ export const fileDownloadTracking = (): EnrichmentPlugin => { } }; const execute = async (event: Event) => event; + const teardown = async () => { + observer?.disconnect(); + removeClickListeners(); + }; return { name, type, setup, execute, + teardown, }; }; diff --git a/packages/analytics-browser/src/plugins/form-interaction-tracking.ts b/packages/analytics-browser/src/plugins/form-interaction-tracking.ts index 380f7061f..359a5bf3c 100644 --- a/packages/analytics-browser/src/plugins/form-interaction-tracking.ts +++ b/packages/analytics-browser/src/plugins/form-interaction-tracking.ts @@ -8,7 +8,31 @@ import { } from '../constants'; import { BrowserConfig } from '../config'; +interface EventListener { + element: Element; + type: 'change' | 'submit'; + handler: () => void; +} + export const formInteractionTracking = (): EnrichmentPlugin => { + let observer: MutationObserver | undefined; + let eventListeners: EventListener[] = []; + const addEventListener = (element: Element, type: 'change' | 'submit', handler: () => void) => { + element.addEventListener(type, handler); + eventListeners.push({ + element, + type, + handler, + }); + }; + const removeClickListeners = () => { + eventListeners.forEach(({ element, type, handler }) => { + /* istanbul ignore next */ + element?.removeEventListener(type, handler); + }); + eventListeners = []; + }; + const name = '@amplitude/plugin-form-interaction-tracking-browser'; const type = PluginType.ENRICHMENT; const setup = async (config: BrowserConfig, amplitude?: BrowserClient) => { @@ -21,25 +45,26 @@ export const formInteractionTracking = (): EnrichmentPlugin => { return; } + /* istanbul ignore if */ + if (typeof document === 'undefined') { + return; + } + const addFormInteractionListener = (form: HTMLFormElement) => { let hasFormChanged = false; - form.addEventListener( - 'change', - () => { - if (!hasFormChanged) { - amplitude.track(DEFAULT_FORM_START_EVENT, { - [FORM_ID]: form.id, - [FORM_NAME]: form.name, - [FORM_DESTINATION]: form.action, - }); - } - hasFormChanged = true; - }, - {}, - ); + addEventListener(form, 'change', () => { + if (!hasFormChanged) { + amplitude.track(DEFAULT_FORM_START_EVENT, { + [FORM_ID]: form.id, + [FORM_NAME]: form.name, + [FORM_DESTINATION]: form.action, + }); + } + hasFormChanged = true; + }); - form.addEventListener('submit', () => { + addEventListener(form, 'submit', () => { if (!hasFormChanged) { amplitude.track(DEFAULT_FORM_START_EVENT, { [FORM_ID]: form.id, @@ -64,7 +89,7 @@ export const formInteractionTracking = (): EnrichmentPlugin => { // Adds listener to anchor tags added after initial load /* istanbul ignore else */ if (typeof MutationObserver !== 'undefined') { - const observer = new MutationObserver((mutations) => { + observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeName === 'FORM') { @@ -84,11 +109,16 @@ export const formInteractionTracking = (): EnrichmentPlugin => { } }; const execute = async (event: Event) => event; + const teardown = async () => { + observer?.disconnect(); + removeClickListeners(); + }; return { name, type, setup, execute, + teardown, }; }; diff --git a/packages/analytics-browser/test/plugins/file-download-tracking.test.ts b/packages/analytics-browser/test/plugins/file-download-tracking.test.ts index 513e720ff..27bc8ee23 100644 --- a/packages/analytics-browser/test/plugins/file-download-tracking.test.ts +++ b/packages/analytics-browser/test/plugins/file-download-tracking.test.ts @@ -29,7 +29,7 @@ describe('fileDownloadTracking', () => { const plugin = fileDownloadTracking(); await plugin.setup(config, amplitude); - // trigger change event + // trigger click event document.getElementById('my-link-id')?.dispatchEvent(new Event('click')); // assert file download event was tracked @@ -41,6 +41,15 @@ describe('fileDownloadTracking', () => { [LINK_TEXT]: 'my-link-text', [LINK_URL]: 'https://analytics.amplitude.com/files/my-file.pdf', }); + + // stop observer and listeners + await plugin.teardown?.(); + + // trigger click event + document.getElementById('my-link-id')?.dispatchEvent(new Event('click')); + + // assert no additional event was tracked + expect(amplitude.track).toHaveBeenCalledTimes(1); }); test('should track file_download event for a dynamically added achor tag', async () => { @@ -71,6 +80,25 @@ describe('fileDownloadTracking', () => { [LINK_TEXT]: 'my-link-2-text', [LINK_URL]: 'https://analytics.amplitude.com/files/my-file-2.pdf', }); + + // stop observer and listeners + await plugin.teardown?.(); + + // add anchor element dynamically + const link3 = document.createElement('a'); + link3.setAttribute('id', 'my-link-3-id'); + link3.setAttribute('class', 'my-link-3-class'); + link3.setAttribute('href', 'https://analytics.amplitude.com/files/my-file-3.pdf'); + link3.text = 'my-link-3-text'; + document.body.appendChild(link3); + + // allow mutation observer to execute and event listener to be attached + await new Promise((r) => r(undefined)); // basically, await next clock tick + // trigger change event + link.dispatchEvent(new Event('click')); + + // assert no additional file download event was tracked + expect(amplitude.track).toHaveBeenCalledTimes(1); }); test('should track file_download event for a dynamically added nested achor tag', async () => { @@ -130,4 +158,12 @@ describe('fileDownloadTracking', () => { const result = await plugin.execute(input); expect(result).toEqual(input); }); + + // eslint-disable-next-line jest/expect-expect + test('should teardown plugin', async () => { + const plugin = fileDownloadTracking(); + await plugin.teardown?.(); + // no explicit assertion + // test asserts that no error is thrown + }); }); diff --git a/packages/analytics-browser/test/plugins/form-interaction-tracking.test.ts b/packages/analytics-browser/test/plugins/form-interaction-tracking.test.ts index 238b060bc..9a3f200ce 100644 --- a/packages/analytics-browser/test/plugins/form-interaction-tracking.test.ts +++ b/packages/analytics-browser/test/plugins/form-interaction-tracking.test.ts @@ -98,6 +98,35 @@ describe('formInteractionTracking', () => { // assert second event was not tracked expect(amplitude.track).toHaveBeenCalledTimes(1); + + // stop observer and listeners + await plugin.teardown?.(); + + // add form element dynamically + const form3 = document.createElement('form'); + form3.setAttribute('id', 'my-form-3-id'); + form3.setAttribute('name', 'my-form-3-name'); + form3.setAttribute('action', '/submit'); + + const text3 = document.createElement('input'); + text3.setAttribute('type', 'text'); + text3.setAttribute('id', 'my-text-3-id'); + + const submit3 = document.createElement('input'); + submit3.setAttribute('type', 'submit'); + submit3.setAttribute('id', 'my-submit-3-id'); + + form3.appendChild(text3); + form3.appendChild(submit3); + document.body.appendChild(form3); + + // allow mutation observer to execute and event listener to be attached + await new Promise((r) => r(undefined)); // basically, await next clock tick + // trigger change event + form3.dispatchEvent(new Event('change')); + + // assert no additional event was tracked + expect(amplitude.track).toHaveBeenCalledTimes(1); }); test('should track form_start event for a dynamically added nested form tag', async () => { @@ -154,16 +183,22 @@ describe('formInteractionTracking', () => { const plugin = formInteractionTracking(); await plugin.setup(config, amplitude); - // trigger change event - document.getElementById('my-form-id')?.dispatchEvent(new Event('submit')); + // trigger change event again + document.getElementById('my-form-id')?.dispatchEvent(new Event('change')); - // assert both events were tracked - expect(amplitude.track).toHaveBeenCalledTimes(2); + // assert first event was tracked + expect(amplitude.track).toHaveBeenCalledTimes(1); expect(amplitude.track).toHaveBeenNthCalledWith(1, '[Amplitude] Form Started', { [FORM_ID]: 'my-form-id', [FORM_NAME]: 'my-form-name', [FORM_DESTINATION]: 'http://localhost/submit', }); + + // trigger submit event + document.getElementById('my-form-id')?.dispatchEvent(new Event('submit')); + + // assert second event was tracked + expect(amplitude.track).toHaveBeenCalledTimes(2); expect(amplitude.track).toHaveBeenNthCalledWith(2, '[Amplitude] Form Submitted', { [FORM_ID]: 'my-form-id', [FORM_NAME]: 'my-form-name', @@ -175,29 +210,32 @@ describe('formInteractionTracking', () => { // setup const config = createConfigurationMock(); const plugin = formInteractionTracking(); - await plugin.setup(config, amplitude); + await plugin.setup?.(config, amplitude); - // trigger change event again - document.getElementById('my-form-id')?.dispatchEvent(new Event('change')); + // trigger change event + document.getElementById('my-form-id')?.dispatchEvent(new Event('submit')); - // assert first event was tracked - expect(amplitude.track).toHaveBeenCalledTimes(1); + // assert both events were tracked + expect(amplitude.track).toHaveBeenCalledTimes(2); expect(amplitude.track).toHaveBeenNthCalledWith(1, '[Amplitude] Form Started', { [FORM_ID]: 'my-form-id', [FORM_NAME]: 'my-form-name', [FORM_DESTINATION]: 'http://localhost/submit', }); - - // trigger submit event - document.getElementById('my-form-id')?.dispatchEvent(new Event('submit')); - - // assert second event was tracked - expect(amplitude.track).toHaveBeenCalledTimes(2); expect(amplitude.track).toHaveBeenNthCalledWith(2, '[Amplitude] Form Submitted', { [FORM_ID]: 'my-form-id', [FORM_NAME]: 'my-form-name', [FORM_DESTINATION]: 'http://localhost/submit', }); + + // stop observer and listeners + await plugin.teardown?.(); + + // trigger change event + document.getElementById('my-form-id')?.dispatchEvent(new Event('submit')); + + // assert no additional event was tracked + expect(amplitude.track).toHaveBeenCalledTimes(2); }); test('should not enrich events', async () => { @@ -208,4 +246,12 @@ describe('formInteractionTracking', () => { const result = await plugin.execute(input); expect(result).toEqual(input); }); + + // eslint-disable-next-line jest/expect-expect + test('should teardown plugin', async () => { + const plugin = formInteractionTracking(); + await plugin.teardown?.(); + // no explicit assertion + // test asserts that no error is thrown + }); }); diff --git a/packages/analytics-core/src/timeline.ts b/packages/analytics-core/src/timeline.ts index 16f37346d..daea7bd13 100644 --- a/packages/analytics-core/src/timeline.ts +++ b/packages/analytics-core/src/timeline.ts @@ -27,16 +27,17 @@ export class Timeline { this.plugins.push(plugin); } - deregister(pluginName: string) { - this.plugins.splice( - this.plugins.findIndex((plugin) => plugin.name === pluginName), - 1, - ); - return Promise.resolve(); + async deregister(pluginName: string) { + const index = this.plugins.findIndex((plugin) => plugin.name === pluginName); + const plugin = this.plugins[index]; + this.plugins.splice(index, 1); + await plugin.teardown?.(); } reset(client: CoreClient) { this.applying = false; + const plugins = this.plugins; + plugins.map((plugin) => plugin.teardown?.()); this.plugins = []; this.client = client; } diff --git a/packages/analytics-core/test/timeline.test.ts b/packages/analytics-core/test/timeline.test.ts index 6f53d1915..397205cb3 100644 --- a/packages/analytics-core/test/timeline.test.ts +++ b/packages/analytics-core/test/timeline.test.ts @@ -3,6 +3,7 @@ import { Event, Plugin, PluginType } from '@amplitude/analytics-types'; import { useDefaultConfig, promiseState } from './helpers/default'; import { createTrackEvent } from '../src/utils/event-builder'; import { AmplitudeCore } from '../src/core-client'; +import { UUID } from '../src/utils/uuid'; describe('timeline', () => { let timeline = new Timeline(new AmplitudeCore()); @@ -11,8 +12,54 @@ describe('timeline', () => { timeline = new Timeline(new AmplitudeCore()); }); + describe('reset', () => { + test('should reset timeline', () => { + const timeline = new Timeline(new AmplitudeCore()); + timeline.plugins = []; + timeline.reset(new AmplitudeCore()); + expect(timeline.applying).toEqual(false); + expect(timeline.plugins).toEqual([]); + }); + + test('should reset timeline without plugin.teardown', () => { + const setup = jest.fn(); + const timeline = new Timeline(new AmplitudeCore()); + timeline.plugins = [ + { + name: UUID(), + type: PluginType.BEFORE, + setup, + execute: async (e) => e, + }, + ]; + timeline.reset(new AmplitudeCore()); + expect(setup).toHaveBeenCalledTimes(0); + expect(timeline.applying).toEqual(false); + expect(timeline.plugins).toEqual([]); + }); + + test('should reset timeline with plugin.teardown', () => { + const teardown = jest.fn(); + const timeline = new Timeline(new AmplitudeCore()); + timeline.plugins = [ + { + name: UUID(), + type: PluginType.BEFORE, + setup: async () => undefined, + execute: async (e) => e, + teardown, + }, + ]; + timeline.reset(new AmplitudeCore()); + expect(teardown).toHaveBeenCalledTimes(1); + expect(timeline.applying).toEqual(false); + expect(timeline.plugins).toEqual([]); + }); + }); + test('should update event using before/enrichment plugin', async () => { const beforeSetup = jest.fn().mockReturnValue(Promise.resolve()); + const beforeTeardown = jest.fn().mockReturnValue(Promise.resolve()); const beforeExecute = jest.fn().mockImplementation((event: Event) => Promise.resolve({ ...event, @@ -23,6 +70,7 @@ describe('timeline', () => { name: 'plugin:before', type: PluginType.BEFORE, setup: beforeSetup, + teardown: beforeTeardown, execute: beforeExecute, }; const enrichmentSetup = jest.fn().mockReturnValue(Promise.resolve()); diff --git a/packages/analytics-types/src/plugin.ts b/packages/analytics-types/src/plugin.ts index b40801223..bd4d8e04f 100644 --- a/packages/analytics-types/src/plugin.ts +++ b/packages/analytics-types/src/plugin.ts @@ -14,6 +14,7 @@ export interface BeforePlugin { type: PluginType.BEFORE; setup(config: U, client?: T): Promise; execute(context: Event): Promise; + teardown?(): Promise; } export interface EnrichmentPlugin { @@ -21,6 +22,7 @@ export interface EnrichmentPlugin { type: PluginType.ENRICHMENT; setup(config: U, client?: T): Promise; execute(context: Event): Promise; + teardown?(): Promise; } export interface DestinationPlugin { @@ -29,6 +31,7 @@ export interface DestinationPlugin { setup(config: U, client?: T): Promise; execute(context: Event): Promise; flush?(): Promise; + teardown?(): Promise; } export type Plugin = BeforePlugin | EnrichmentPlugin | DestinationPlugin; diff --git a/packages/plugin-page-view-tracking-browser/src/page-view-tracking.ts b/packages/plugin-page-view-tracking-browser/src/page-view-tracking.ts index a7c639b4a..d9216aad5 100644 --- a/packages/plugin-page-view-tracking-browser/src/page-view-tracking.ts +++ b/packages/plugin-page-view-tracking-browser/src/page-view-tracking.ts @@ -23,6 +23,7 @@ export const pageViewTrackingPlugin: CreatePageViewTrackingPlugin = ( let options: Options = {}; const globalScope = getGlobalScope(); let loggerProvider: Logger | undefined = undefined; + let pushState: undefined | ((data: any, unused: string, url?: string | URL | null) => void); const [clientOrOptions, configOrUndefined] = args; if (clientOrOptions && 'init' in clientOrOptions) { @@ -65,6 +66,11 @@ export const pageViewTrackingPlugin: CreatePageViewTrackingPlugin = ( previousURL = newURL; }; + /* istanbul ignore next */ + const trackHistoryPageViewWrapper = () => { + void trackHistoryPageView(); + }; + const plugin = { name: 'page-view-tracking', type: PluginType.ENRICHMENT as const, @@ -93,9 +99,11 @@ export const pageViewTrackingPlugin: CreatePageViewTrackingPlugin = ( if (options.trackHistoryChanges && globalScope) { /* istanbul ignore next */ - globalScope.addEventListener('popstate', () => { - void trackHistoryPageView(); - }); + globalScope.addEventListener('popstate', trackHistoryPageViewWrapper); + + // Save reference to original push state, to be used in teardown + // eslint-disable-next-line @typescript-eslint/unbound-method + pushState = globalScope.history.pushState; /* istanbul ignore next */ // There is no global browser listener for changes to history, so we have @@ -130,6 +138,15 @@ export const pageViewTrackingPlugin: CreatePageViewTrackingPlugin = ( } return event; }, + + teardown: async () => { + if (globalScope) { + globalScope.removeEventListener('popstate', trackHistoryPageViewWrapper); + if (pushState) { + globalScope.history.pushState = pushState; + } + } + }, }; // Required for unit tests diff --git a/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts b/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts index 187f6a057..479f45be4 100644 --- a/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts +++ b/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts @@ -488,6 +488,34 @@ describe('pageViewTrackingPlugin', () => { }); }); + describe('teardown', () => { + test('should call remove listeners', async () => { + const amplitude = createInstance(); + const removeEventListener = jest.spyOn(window, 'removeEventListener'); + const loggerProvider: Partial = { + log: jest.fn(), + }; + const config: Partial = { + loggerProvider: loggerProvider as Logger, + }; + const plugin = pageViewTrackingPlugin({ + trackHistoryChanges: 'all', + }); + await plugin.setup(config as Config, amplitude); + await plugin.teardown?.(); + expect(removeEventListener).toHaveBeenCalledTimes(1); + }); + + test('should call remove listeners without proxy', async () => { + const removeEventListener = jest.spyOn(window, 'removeEventListener'); + const plugin = pageViewTrackingPlugin({ + trackHistoryChanges: 'all', + }); + await plugin.teardown?.(); + expect(removeEventListener).toHaveBeenCalledTimes(1); + }); + }); + test('shouldTrackHistoryPageView pathOnly option', () => { const url1 = 'https://www.example.com/path/to/page'; const url2 = 'https://www.example.com/path/to/page?query=1'; From 603e3eff81a3d03082544541a673df955cf30118 Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Wed, 5 Jul 2023 09:06:16 -0700 Subject: [PATCH 046/214] fix: allow literal values for plugin type (#468) --- packages/analytics-types/src/plugin.ts | 6 +++--- .../plugin-session-replay-browser/src/session-replay.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/analytics-types/src/plugin.ts b/packages/analytics-types/src/plugin.ts index bd4d8e04f..80d83386e 100644 --- a/packages/analytics-types/src/plugin.ts +++ b/packages/analytics-types/src/plugin.ts @@ -11,7 +11,7 @@ export enum PluginType { export interface BeforePlugin { name: string; - type: PluginType.BEFORE; + type: PluginType.BEFORE | 'before'; setup(config: U, client?: T): Promise; execute(context: Event): Promise; teardown?(): Promise; @@ -19,7 +19,7 @@ export interface BeforePlugin { export interface EnrichmentPlugin { name: string; - type: PluginType.ENRICHMENT; + type: PluginType.ENRICHMENT | 'enrichment'; setup(config: U, client?: T): Promise; execute(context: Event): Promise; teardown?(): Promise; @@ -27,7 +27,7 @@ export interface EnrichmentPlugin { export interface DestinationPlugin { name: string; - type: PluginType.DESTINATION; + type: PluginType.DESTINATION | 'destination'; setup(config: U, client?: T): Promise; execute(context: Event): Promise; flush?(): Promise; diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index d4d1926c7..2808f23c9 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -1,5 +1,5 @@ import { AMPLITUDE_PREFIX, BaseTransport } from '@amplitude/analytics-core'; -import { BrowserConfig, Event, PluginType, Status } from '@amplitude/analytics-types'; +import { BrowserConfig, Event, Status } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import { pack, record } from 'rrweb'; import { DEFAULT_SESSION_END_EVENT, DEFAULT_SESSION_REPLAY_PROPERTY, DEFAULT_SESSION_START_EVENT } from './constants'; @@ -20,7 +20,7 @@ const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BY class SessionReplay implements SessionReplayEnrichmentPlugin { name = '@amplitude/plugin-session-replay-browser'; - type = PluginType.ENRICHMENT as const; + type = 'enrichment' as const; // this.config is defined in setup() which will always be called first // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore From 712158b1936d302a88f5d651dd13d4b1ff76c70b Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Mon, 3 Jul 2023 14:11:09 -0700 Subject: [PATCH 047/214] fix: handle concurrent cookie testing (#465) --- packages/analytics-client-common/src/storage/cookie.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/analytics-client-common/src/storage/cookie.ts b/packages/analytics-client-common/src/storage/cookie.ts index 6d0ce8c26..6d48cb8de 100644 --- a/packages/analytics-client-common/src/storage/cookie.ts +++ b/packages/analytics-client-common/src/storage/cookie.ts @@ -3,6 +3,7 @@ import { getGlobalScope } from '../global-scope'; export class CookieStorage implements Storage { options: CookieStorageOptions; + private static testValue: undefined | string; constructor(options?: CookieStorageOptions) { this.options = { ...options }; @@ -14,13 +15,13 @@ export class CookieStorage implements Storage { return false; } - const random = String(Date.now()); + CookieStorage.testValue = String(Date.now()); const testStrorage = new CookieStorage(this.options); const testKey = 'AMP_TEST'; try { - await testStrorage.set(testKey, random); + await testStrorage.set(testKey, CookieStorage.testValue); const value = await testStrorage.get(testKey); - return value === random; + return value === CookieStorage.testValue; } catch { /* istanbul ignore next */ return false; From 4ebf96b8c611cc0c766446b35e8a209bf5af580b Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Mon, 3 Jul 2023 14:11:19 -0700 Subject: [PATCH 048/214] fix: handle concurrent history push state calls (#466) --- .../src/page-view-tracking.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/plugin-page-view-tracking-browser/src/page-view-tracking.ts b/packages/plugin-page-view-tracking-browser/src/page-view-tracking.ts index d9216aad5..26947d6b7 100644 --- a/packages/plugin-page-view-tracking-browser/src/page-view-tracking.ts +++ b/packages/plugin-page-view-tracking-browser/src/page-view-tracking.ts @@ -57,13 +57,19 @@ export const pageViewTrackingPlugin: CreatePageViewTrackingPlugin = ( const trackHistoryPageView = async (): Promise => { const newURL = location.href; + const shouldTrackPageView = + shouldTrackHistoryPageView(options.trackHistoryChanges, newURL, previousURL || '') && shouldTrackOnPageLoad(); + // Note: Update `previousURL` in the same clock tick as `shouldTrackHistoryPageView()` + // This was previously done after `amplitude?.track(await createPageViewEvent());` and + // causes a concurrency issue where app triggers `pushState` twice with the same URL target + // but `previousURL` is only updated after the second `pushState` producing two page viewed events + previousURL = newURL; - if (shouldTrackHistoryPageView(options.trackHistoryChanges, newURL, previousURL || '') && shouldTrackOnPageLoad()) { + if (shouldTrackPageView) { /* istanbul ignore next */ loggerProvider?.log('Tracking page view event'); amplitude?.track(await createPageViewEvent()); } - previousURL = newURL; }; /* istanbul ignore next */ From bc426d86b020aa96c9dc3a824aaaecf22213b32f Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Wed, 5 Jul 2023 16:45:31 +0000 Subject: [PATCH 049/214] chore(release): publish - @amplitude/analytics-browser-test@1.4.7 - @amplitude/analytics-browser@1.12.1 - @amplitude/analytics-client-common@1.1.1 - @amplitude/analytics-core@1.2.1 - @amplitude/analytics-node-test@1.0.5 - @amplitude/analytics-node@1.3.1 - @amplitude/analytics-react-native@1.3.1 - @amplitude/analytics-types@1.3.1 - @amplitude/marketing-analytics-browser@1.0.7 - @amplitude/plugin-page-view-tracking-browser@1.0.7 - @amplitude/plugin-session-replay-browser@0.1.1 - @amplitude/plugin-web-attribution-browser@1.0.7 --- packages/analytics-browser-test/CHANGELOG.md | 9 +++++++++ packages/analytics-browser-test/package.json | 4 ++-- packages/analytics-browser/CHANGELOG.md | 12 ++++++++++++ packages/analytics-browser/README.md | 2 +- .../generated/amplitude-snippet.js | 4 ++-- packages/analytics-browser/package.json | 12 ++++++------ packages/analytics-browser/playground/amplitude.js | 2 +- packages/analytics-browser/src/version.ts | 2 +- packages/analytics-client-common/CHANGELOG.md | 12 ++++++++++++ packages/analytics-client-common/package.json | 6 +++--- packages/analytics-core/CHANGELOG.md | 12 ++++++++++++ packages/analytics-core/package.json | 4 ++-- packages/analytics-node-test/CHANGELOG.md | 9 +++++++++ packages/analytics-node-test/package.json | 4 ++-- packages/analytics-node/CHANGELOG.md | 9 +++++++++ packages/analytics-node/package.json | 6 +++--- packages/analytics-node/src/version.ts | 2 +- packages/analytics-react-native/CHANGELOG.md | 9 +++++++++ packages/analytics-react-native/package.json | 8 ++++---- packages/analytics-react-native/src/version.ts | 2 +- packages/analytics-types/CHANGELOG.md | 14 ++++++++++++++ packages/analytics-types/package.json | 2 +- packages/marketing-analytics-browser/CHANGELOG.md | 9 +++++++++ packages/marketing-analytics-browser/README.md | 2 +- .../generated/amplitude-gtm-snippet.js | 4 ++-- .../generated/amplitude-snippet.js | 4 ++-- packages/marketing-analytics-browser/package.json | 14 +++++++------- .../marketing-analytics-browser/src/version.ts | 2 +- .../plugin-page-view-tracking-browser/CHANGELOG.md | 14 ++++++++++++++ .../plugin-page-view-tracking-browser/package.json | 8 ++++---- .../plugin-session-replay-browser/CHANGELOG.md | 12 ++++++++++++ .../plugin-session-replay-browser/package.json | 8 ++++---- .../plugin-web-attribution-browser/CHANGELOG.md | 12 ++++++++++++ .../plugin-web-attribution-browser/package.json | 10 +++++----- 34 files changed, 189 insertions(+), 56 deletions(-) diff --git a/packages/analytics-browser-test/CHANGELOG.md b/packages/analytics-browser-test/CHANGELOG.md index 03ef9016c..d4c19e1f4 100644 --- a/packages/analytics-browser-test/CHANGELOG.md +++ b/packages/analytics-browser-test/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.4.7](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.6...@amplitude/analytics-browser-test@1.4.7) (2023-07-05) + +**Note:** Version bump only for package @amplitude/analytics-browser-test + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.4.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.5...@amplitude/analytics-browser-test@1.4.6) (2023-06-26) **Note:** Version bump only for package @amplitude/analytics-browser-test diff --git a/packages/analytics-browser-test/package.json b/packages/analytics-browser-test/package.json index 96b473245..253db8a1a 100644 --- a/packages/analytics-browser-test/package.json +++ b/packages/analytics-browser-test/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser-test", - "version": "1.4.6", + "version": "1.4.7", "private": true, "description": "", "author": "Amplitude Inc", @@ -16,7 +16,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.12.0" + "@amplitude/analytics-browser": "^1.12.1" }, "devDependencies": { "nock": "^13.2.4" diff --git a/packages/analytics-browser/CHANGELOG.md b/packages/analytics-browser/CHANGELOG.md index a8b538e4b..021c0ed45 100644 --- a/packages/analytics-browser/CHANGELOG.md +++ b/packages/analytics-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.12.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.12.0...@amplitude/analytics-browser@1.12.1) (2023-07-05) + +### Bug Fixes + +- allow plugins to teardown to remove listeners ([#463](https://github.com/amplitude/Amplitude-TypeScript/issues/463)) + ([1394ddb](https://github.com/amplitude/Amplitude-TypeScript/commit/1394ddb3775c7e5b1a3da87e6a4c973a586c6382)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [1.12.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.11.2...@amplitude/analytics-browser@1.12.0) (2023-06-26) ### Features diff --git a/packages/analytics-browser/README.md b/packages/analytics-browser/README.md index a43a16b2c..3d7c89b8d 100644 --- a/packages/analytics-browser/README.md +++ b/packages/analytics-browser/README.md @@ -40,7 +40,7 @@ Alternatively, the package is also distributed through a CDN. Copy and paste the ```html diff --git a/packages/analytics-browser/generated/amplitude-snippet.js b/packages/analytics-browser/generated/amplitude-snippet.js index 6c6c42737..bb02adfd8 100644 --- a/packages/analytics-browser/generated/amplitude-snippet.js +++ b/packages/analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-s4Wx3hZn20lk10XSCWAIMwlpJRoowQ1Yt6E0gUEhrmytlmyODAskmfnOcu84UYAg'; + as.integrity = 'sha384-tVVRWU7GrpjrC44WiDzQSQ9/fCEp3KlzT6HvGeU9Q4YPkOziz0qa/azi73J6jBr6'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.12.0-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.12.1-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/analytics-browser/package.json b/packages/analytics-browser/package.json index d73e6b7e7..105308739 100644 --- a/packages/analytics-browser/package.json +++ b/packages/analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser", - "version": "1.12.0", + "version": "1.12.1", "description": "Official Amplitude SDK for Web", "keywords": [ "analytics", @@ -44,11 +44,11 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.1.0", - "@amplitude/analytics-core": "^1.2.0", - "@amplitude/analytics-types": "^1.3.0", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.6", - "@amplitude/plugin-web-attribution-browser": "^1.0.6", + "@amplitude/analytics-client-common": "^1.1.1", + "@amplitude/analytics-core": "^1.2.1", + "@amplitude/analytics-types": "^1.3.1", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.7", + "@amplitude/plugin-web-attribution-browser": "^1.0.7", "@amplitude/ua-parser-js": "^0.7.31", "tslib": "^2.4.1" }, diff --git a/packages/analytics-browser/playground/amplitude.js b/packages/analytics-browser/playground/amplitude.js index 17b09d826..32e9b690f 100644 --- a/packages/analytics-browser/playground/amplitude.js +++ b/packages/analytics-browser/playground/amplitude.js @@ -1 +1 @@ -!function(){"use strict";var e=function(t,i){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])},e(t,i)};function t(t,i){if("function"!=typeof i&&null!==i)throw new TypeError("Class extends value "+String(i)+" is not a constructor or null");function n(){this.constructor=t}e(t,i),t.prototype=null===i?Object.create(i):(n.prototype=i.prototype,new n)}var i=function(){return i=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!L(t,i))return!1}return!0},L=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&j(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return j(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},M=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||L(t,i)))},e}(),V=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},z=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},F=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){return this.plugins.splice(this.plugins.findIndex((function(t){return t.name===e})),1),Promise.resolve()},e.prototype.reset=function(e){this.applying=!1,this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,k,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),k={error:y},[3,16];case 15:try{g&&!g.done&&(E=h.return)&&E.call(h)}finally{if(k)throw k.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return z(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),B="Event rejected due to exceeded retry count",$=function(e){return{promise:e||Promise.resolve()}},Q=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new F(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return $(this.dispatch(r))},e.prototype.identify=function(e,t){var i=V(e,t);return $(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return $(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new M;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return $(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return $(this.dispatch(n))},e.prototype.add=function(e){return this.config?$(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),$())},e.prototype.remove=function(e){return this.config?$(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),$())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(z(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,z(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=z(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return $(this.timeline.flush())},e}(),K=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return j(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),H="Amplitude Logger ",W=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=Y(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:return null===(s=o.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(s,e),[3,4]):("body"in s?this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(X(s))):this.fulfillRequest(e,s.statusCode,s.status),[2]);case 3:return a=o.sent(),u=(c=a)instanceof Error?c.message:String(c),this.config.loggerProvider.error(u),this.fulfillRequest(e,0,u),[3,4];case 4:return[2]}var c}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(X(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));d.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(z(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ne=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},re=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},oe=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},se=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=oe(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ae=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,ue)},ce=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),le=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,k;if("object"!=typeof e)return null;var E=e.code||0,S=this.buildStatus(E);switch(S){case h.Success:return{status:S,statusCode:E,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:E,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:E,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:E,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(k=e.throttled_events)&&void 0!==k?k:[]}};case h.Timeout:default:return{status:S,statusCode:E}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),de=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[D,t,e.substring(0,i)].filter(Boolean).join("_")},pe=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e(this.options),n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),fe=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(le),ve="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},he={exports:{}};ee=he,te=he.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",T="Huawei",O="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",U="Sony",q="Xiaomi",D="Zebra",A="Facebook",C=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},z=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?M(e,275):e,this},this.setUA(f),this};$.VERSION="0.7.31",$.BROWSER=C([a,l,"major"]),$.CPU=C([d]),$.DEVICE=C([s,c,u,p,f,h,v,g,b]),$.ENGINE=$.OS=C([a,l]),ee.exports&&(te=ee.exports=$),te.UAParser=$;var Q=typeof e!==n&&(e.jQuery||e.Zepto);if(Q&&!Q.ua){var K=new $;Q.ua=K.getResult(),Q.ua.get=function(){return K.getUA()},Q.ua.set=function(e){K.setUA(e);var t=K.getResult();for(var i in t)Q.ua[i]=t[i]}}}("object"==typeof window?window:ve);var ge=he.exports,be=function(){function e(){this.ua=new he.exports.UAParser("undefined"!=typeof navigator?navigator.userAgent:null).getResult()}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:we(),platform:"Web",os:ye(this.ua),deviceModel:me(this.ua)}},e}(),ye=function(e){var t,i;return[null===(t=e.browser)||void 0===t?void 0:t.name,null===(i=e.browser)||void 0===i?void 0:i.major].filter((function(e){return null!=e})).join(" ")},me=function(e){var t;return null===(t=e.os)||void 0===t?void 0:t.name},we=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},_e=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),Ie=function(){return Ie=Object.assign||function(e){for(var t,i=1,n=arguments.length;i=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(null!==(I=null!==(_=t.sessionId)&&void 0!==_?_:this.config.sessionId)&&void 0!==I?I:Date.now()),B=!0),($=Oe(t.instanceName)).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new ie).promise];case 11:return W.sent(),[4,this.add(new qe).promise];case 12:return W.sent(),[4,this.add(new xe).promise];case 13:return W.sent(),("boolean"==typeof(Z=this.config.defaultTracking)?Z:null==Z?void 0:Z.fileDownloads)?[4,this.add({name:"@amplitude/plugin-file-download-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i,n;return o(this,(function(r){return t?(i=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=n.exec(i.href),o=null==r?void 0:r[1];o&&e.addEventListener("click",(function(){var n;o&&t.track(st,((n={})[ct]=o,n[lt]=i.pathname,n[dt]=e.id,n[pt]=e.text,n[ft]=e.href,n))}))},n=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,15];case 14:W.sent(),W.label=15;case 15:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add({name:"@amplitude/plugin-form-interaction-tracking-browser",type:f.ENRICHMENT,setup:function(e,t){return r(void 0,void 0,void 0,(function(){var i;return o(this,(function(n){return t?(i=function(e){var i=!1;e.addEventListener("change",(function(){var n;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),i=!0}),{}),e.addEventListener("submit",(function(){var n,r;i||t.track(rt,((n={})[vt]=e.id,n[ht]=e.name,n[gt]=e.action,n)),t.track(ot,((r={})[vt]=e.id,r[ht]=e.name,r[gt]=e.action,r)),i=!1}))},Array.from(document.getElementsByTagName("form")).forEach(i),"undefined"!=typeof MutationObserver&&new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&i(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(i)}))}))})).observe(document.body,{subtree:!0,childList:!0}),[2]):(e.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return r(void 0,void 0,void 0,(function(){return o(this,(function(t){return[2,e]}))}))}}).promise]:[3,17];case 16:W.sent(),W.label=17;case 17:return(null===(k=this.config.attribution)||void 0===k?void 0:k.disabled)?[3,19]:(Q=function(){for(var e,t,n=this,s=[],u=0;uthis.config.sessionTimeout&&this.setSessionId(i),[2,e.prototype.process.call(this,t)]}))}))},n}(Q),mt=function(){var e=new yt;return{init:ae(e.init.bind(e),"init",re(e),se(e,["config"])),add:ae(e.add.bind(e),"add",re(e),se(e,["config.apiKey","timeline.plugins"])),remove:ae(e.remove.bind(e),"remove",re(e),se(e,["config.apiKey","timeline.plugins"])),track:ae(e.track.bind(e),"track",re(e),se(e,["config.apiKey","timeline.queue.length"])),logEvent:ae(e.logEvent.bind(e),"logEvent",re(e),se(e,["config.apiKey","timeline.queue.length"])),identify:ae(e.identify.bind(e),"identify",re(e),se(e,["config.apiKey","timeline.queue.length"])),groupIdentify:ae(e.groupIdentify.bind(e),"groupIdentify",re(e),se(e,["config.apiKey","timeline.queue.length"])),setGroup:ae(e.setGroup.bind(e),"setGroup",re(e),se(e,["config.apiKey","timeline.queue.length"])),revenue:ae(e.revenue.bind(e),"revenue",re(e),se(e,["config.apiKey","timeline.queue.length"])),flush:ae(e.flush.bind(e),"flush",re(e),se(e,["config.apiKey","timeline.queue.length"])),getUserId:ae(e.getUserId.bind(e),"getUserId",re(e),se(e,["config","config.userId"])),setUserId:ae(e.setUserId.bind(e),"setUserId",re(e),se(e,["config","config.userId"])),getDeviceId:ae(e.getDeviceId.bind(e),"getDeviceId",re(e),se(e,["config","config.deviceId"])),setDeviceId:ae(e.setDeviceId.bind(e),"setDeviceId",re(e),se(e,["config","config.deviceId"])),reset:ae(e.reset.bind(e),"reset",re(e),se(e,["config","config.userId","config.deviceId"])),getSessionId:ae(e.getSessionId.bind(e),"getSessionId",re(e),se(e,["config"])),setSessionId:ae(e.setSessionId.bind(e),"setSessionId",re(e),se(e,["config"])),extendSession:ae(e.extendSession.bind(e),"extendSession",re(e),se(e,["config"])),setOptOut:ae(e.setOptOut.bind(e),"setOptOut",re(e),se(e,["config"])),setTransport:ae(e.setTransport.bind(e),"setTransport",re(e),se(e,["config"]))}},wt=mt(),_t=wt.add,It=wt.extendSession,kt=wt.flush,Et=wt.getDeviceId,St=wt.getSessionId,Tt=wt.getUserId,Ot=wt.groupIdentify,xt=wt.identify,Pt=wt.init,Rt=wt.logEvent,Nt=wt.remove,Ut=wt.reset,qt=wt.revenue,Dt=wt.setDeviceId,At=wt.setGroup,Ct=wt.setOptOut,jt=wt.setSessionId,Lt=wt.setTransport,Mt=wt.setUserId,Vt=wt.track,zt=Object.freeze({__proto__:null,add:_t,extendSession:It,flush:kt,getDeviceId:Et,getSessionId:St,getUserId:Tt,groupIdentify:Ot,identify:xt,init:Pt,logEvent:Rt,remove:Nt,reset:Ut,revenue:qt,setDeviceId:Dt,setGroup:At,setOptOut:Ct,setSessionId:jt,setTransport:Lt,setUserId:Mt,track:Vt,Types:q,createInstance:mt,runQueuedFunctions:Re,Revenue:K,Identify:M});!function(){var e=b();if(e){var t=function(e){var t=mt(),i=b();return i&&i.amplitude&&i.amplitude._iq&&e&&(i.amplitude._iq[e]=t),t};if(e.amplitude=Object.assign(e.amplitude||{},zt,{createInstance:t}),e.amplitude.invoked){var i=e.amplitude._q;e.amplitude._q=[],Re(zt,i);for(var n=Object.keys(e.amplitude._iq)||[],r=0;r0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function a(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function u(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!L(t,i))return!1}return!0},L=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;if(Array.isArray(u))return!1;if("object"==typeof u)r=r&&j(u);else if(!["number","string"].includes(typeof u))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return j(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},M=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return i({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof i:e===c.UNSET||e===c.REMOVE||L(t,i)))},e}(),V=function(e,t){return i(i({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},z=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=h.Unknown),{event:e,code:t,message:i}},F=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){var t;return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:return i=this.plugins.findIndex((function(t){return t.name===e})),n=this.plugins[i],this.plugins.splice(i,1),[4,null===(t=n.teardown)||void 0===t?void 0:t.call(n)];case 1:return r.sent(),[2]}}))}))},e.prototype.reset=function(e){this.applying=!1,this.plugins.map((function(e){var t;return null===(t=e.teardown)||void 0===t?void 0:t.call(e)})),this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,n,r,u,c,l,d,p,v,h,g,b,y,m,w,_,I,E,k;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=a(e,1),n=t[0],r=a(e,2),u=r[1],c=this.plugins.filter((function(e){return e.type===f.BEFORE})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:[4,d.value.execute(i({},n))];case 3:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return p=o.sent(),_={error:p},[3,8];case 7:try{d&&!d.done&&(I=l.return)&&I.call(l)}finally{if(_)throw _.error}return[7];case 8:v=this.plugins.filter((function(e){return e.type===f.ENRICHMENT})),o.label=9;case 9:o.trys.push([9,14,15,16]),h=s(v),g=h.next(),o.label=10;case 10:return g.done?[3,13]:[4,g.value.execute(i({},n))];case 11:if(null===(b=o.sent()))return u({event:n,code:0,message:""}),[2];n=b,o.label=12;case 12:return g=h.next(),[3,10];case 13:return[3,16];case 14:return y=o.sent(),E={error:y},[3,16];case 15:try{g&&!g.done&&(k=h.return)&&k.call(h)}finally{if(E)throw E.error}return[7];case 16:return m=this.plugins.filter((function(e){return e.type===f.DESTINATION})),w=m.map((function(e){var t=i({},n);return e.execute(t).catch((function(e){return z(t,0,String(e))}))})),Promise.all(w).then((function(e){var t=a(e,1)[0];u(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,i,n=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===f.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),B="Event rejected due to exceeded retry count",$=function(e){return{promise:e||Promise.resolve()}},Q=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new F(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,i,n,r,a,u;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),i=s(t),n=i.next(),o.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:o.sent(),o.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),a={error:r},[3,8];case 7:try{n&&!n.done&&(u=i.return)&&u.call(i)}finally{if(a)throw a.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,n){var r=function(e,t,n){return i(i(i({},"string"==typeof e?{event_type:e}:e),n),t&&{event_properties:t})}(e,t,n);return $(this.dispatch(r))},e.prototype.identify=function(e,t){var i=V(e,t);return $(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,n,r){var o=function(e,t,n,r){var o;return i(i({},r),{event_type:d.GROUP_IDENTIFY,group_properties:n.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,n,r);return $(this.dispatch(o))},e.prototype.setGroup=function(e,t,n){var r=function(e,t,n){var r,o=new M;return o.set(e,t),i(i({},n),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,n);return $(this.dispatch(r))},e.prototype.revenue=function(e,t){var n=function(e,t){return i(i({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return $(this.dispatch(n))},e.prototype.add=function(e){return this.config?$(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),$())},e.prototype.remove=function(e){return this.config?$(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),$())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(z(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,z(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=z(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return $(this.timeline.flush())},e}(),K=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return j(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?i({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),H="Amplitude Logger ",W=function(){function e(){this.logLevel=p.None}return e.prototype.disable=function(){this.logLevel=p.None},e.prototype.enable=function(e){void 0===e&&(e=p.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,i,n,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var i,r,s,a,u;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,n(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},o.label=1;case 1:return o.trys.push([1,3,,4]),r=Y(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,i)];case 2:return null===(s=o.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(s,e),[3,4]):("body"in s?this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(X(s))):this.fulfillRequest(e,s.statusCode,s.status),[2]);case 3:return a=o.sent(),u=(c=a)instanceof Error?c.message:String(c),this.config.loggerProvider.error(u),this.fulfillRequest(e,0,u),[3,4];case 4:return[2]}var c}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case h.Success:this.handleSuccessResponse(e,t);break;case h.Invalid:this.handleInvalidResponse(e,t);break;case h.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case h.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=u(u(u(u([],a(Object.values(e.body.eventsWithInvalidFields)),!1),a(Object.values(e.body.eventsWithMissingFields)),!1),a(Object.values(e.body.eventsWithInvalidIdLengths)),!1),a(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(X(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,u([],a(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),c=new Set(r),l=new Set(o),d=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));d.length>0&&this.config.loggerProvider.warn(X(e)),this.addToQueue.apply(this,u([],a(d),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,u([],a(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(z(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),te=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},ie=function(e){return function(){var t=i({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},ne=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var a=o.value;if(!(a in e))return;e=e[a]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},re=function(e,t){return function(){var i,n,r={};try{for(var o=s(t),a=o.next();!a.done;a=o.next()){var u=a.value;r[u]=ne(e,u)}}catch(e){i={error:e}}finally{try{a&&!a.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},oe=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,se)},ae=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return r(this,void 0,void 0,(function(){return o(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),ue=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,g,b,y,m,w,_,I,E;if("object"!=typeof e)return null;var k=e.code||0,S=this.buildStatus(k);switch(S){case h.Success:return{status:S,statusCode:k,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case h.Invalid:return{status:S,statusCode:k,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case h.PayloadTooLarge:return{status:S,statusCode:k,body:{error:null!==(g=e.error)&&void 0!==g?g:""}};case h.RateLimit:return{status:S,statusCode:k,body:{error:null!==(b=e.error)&&void 0!==b?b:"",epsThreshold:null!==(y=e.eps_threshold)&&void 0!==y?y:0,throttledDevices:null!==(m=e.throttled_devices)&&void 0!==m?m:{},throttledUsers:null!==(w=e.throttled_users)&&void 0!==w?w:{},exceededDailyQuotaDevices:null!==(_=e.exceeded_daily_quota_devices)&&void 0!==_?_:{},exceededDailyQuotaUsers:null!==(I=e.exceeded_daily_quota_users)&&void 0!==I?I:{},throttledEvents:null!==(E=e.throttled_events)&&void 0!==E?E:[]}};case h.Timeout:default:return{status:S,statusCode:k}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?h.Success:429===e?h.RateLimit:413===e?h.PayloadTooLarge:408===e?h.Timeout:e>=400&&e<500?h.Invalid:e>=500?h.Failed:h.Unknown},e}(),ce=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[D,t,e.substring(0,i)].filter(Boolean).join("_")},le=function(){function e(e){this.options=i({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i;return o(this,(function(n){switch(n.label){case 0:if(!b())return[2,!1];e.testValue=String(Date.now()),t=new e(this.options),i="AMP_TEST",n.label=1;case 1:return n.trys.push([1,4,5,7]),[4,t.set(i,e.testValue)];case 2:return n.sent(),[4,t.get(i)];case 3:return[2,n.sent()===e.testValue];case 4:return n.sent(),[2,!1];case 5:return[4,t.remove(i)];case 6:return n.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){var i,n,r;return o(this,(function(o){return i=b(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){var n,r,s,a,u,c;return o(this,(function(o){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,s=void 0,(r=null!==t?n:-1)&&((a=new Date).setTime(a.getTime()+24*r*60*60*1e3),s=a),u="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(u+="; expires=".concat(s.toUTCString())),u+="; path=/",this.options.domain&&(u+="; domain=".concat(this.options.domain)),this.options.secure&&(u+="; Secure"),this.options.sameSite&&(u+="; SameSite=".concat(this.options.sameSite)),(c=b())&&(c.document.cookie=u)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),de=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i,n;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},i}(ue),pe=function(){function e(){}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:fe(),platform:"Web",os:void 0,deviceModel:void 0}},e}(),fe=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},ve=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),he=function(){return he=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},z=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o275?M(e,275):e,this},this.setUA(f),this};$.VERSION="0.7.31",$.BROWSER=C([a,l,"major"]),$.CPU=C([d]),$.DEVICE=C([s,c,u,p,f,h,v,g,b]),$.ENGINE=$.OS=C([a,l]),be.exports&&(ye=be.exports=$),ye.UAParser=$;var Q=typeof e!==n&&(e.jQuery||e.Zepto);if(Q&&!Q.ua){var K=new $;Q.ua=K.getResult(),Q.ua.get=function(){return K.getUA()},Q.ua.set=function(e){K.setUA(e);var t=K.getResult();for(var i in t)Q.ua[i]=t[i]}}}("object"==typeof window?window:xe);var Re=Pe.exports,Ne=function(){function e(){this.name="context",this.type=f.BEFORE,this.library="amplitude-ts/".concat("1.12.1"),"undefined"!=typeof navigator&&(this.userAgent=navigator.userAgent),this.uaResult=new Re(this.userAgent).getResult()}return e.prototype.setup=function(e){return this.config=e,Promise.resolve(void 0)},e.prototype.execute=function(e){var t,n;return r(this,void 0,void 0,(function(){var r,s,a,u,c,l,d;return o(this,(function(o){return r=(new Date).getTime(),s=this.uaResult.browser.name,a=this.uaResult.browser.version,u=this.uaResult.device.model||this.uaResult.os.name,c=this.uaResult.device.vendor,l=null!==(t=this.config.lastEventId)&&void 0!==t?t:-1,d=null!==(n=e.event_id)&&void 0!==n?n:l+1,this.config.lastEventId=d,e.time||(this.config.lastEventTime=r),[2,i(i(i(i(i(i(i(i(i(i(i(i({user_id:this.config.userId,device_id:this.config.deviceId,session_id:this.config.sessionId,time:r},this.config.appVersion&&{app_version:this.config.appVersion}),this.config.trackingOptions.platform&&{platform:"Web"}),this.config.trackingOptions.osName&&{os_name:s}),this.config.trackingOptions.osVersion&&{os_version:a}),this.config.trackingOptions.deviceManufacturer&&{device_manufacturer:c}),this.config.trackingOptions.deviceModel&&{device_model:u}),this.config.trackingOptions.language&&{language:ke()}),this.config.trackingOptions.ipAddress&&{ip:"$remote"}),{insert_id:se(),partner_id:this.config.partnerId,plan:this.config.plan}),this.config.ingestionMetadata&&{ingestion_metadata:{source_name:this.config.ingestionMetadata.sourceName,source_version:this.config.ingestionMetadata.sourceVersion}}),e),{event_id:d,library:this.library,user_agent:this.userAgent})]}))}))},e}(),qe=function(){function e(){}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,i,n;return o(this,(function(r){switch(r.label){case 0:if(!b())return[2,!1];t=String(Date.now()),i=new e,n="AMP_TEST",r.label=1;case 1:return r.trys.push([1,4,5,7]),[4,i.set(n,t)];case 2:return r.sent(),[4,i.get(n)];case 3:return[2,r.sent()===t];case 4:return r.sent(),[2,!1];case 5:return[4,i.remove(n)];case 6:return r.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(i){switch(i.label){case 0:return i.trys.push([0,2,,3]),[4,this.getRaw(e)];case 1:return(t=i.sent())?[2,JSON.parse(t)]:[2,void 0];case 2:return i.sent(),[2,void 0];case 3:return[2]}}))}))},e.prototype.getRaw=function(e){var t;return r(this,void 0,void 0,(function(){return o(this,(function(i){return[2,(null===(t=b())||void 0===t?void 0:t.localStorage.getItem(e))||void 0]}))}))},e.prototype.set=function(e,t){var i;return r(this,void 0,void 0,(function(){return o(this,(function(n){try{null===(i=b())||void 0===i||i.localStorage.setItem(e,JSON.stringify(t))}catch(e){}return[2]}))}))},e.prototype.remove=function(e){var t;return r(this,void 0,void 0,(function(){return o(this,(function(i){try{null===(t=b())||void 0===t||t.localStorage.removeItem(e)}catch(e){}return[2]}))}))},e.prototype.reset=function(){var e;return r(this,void 0,void 0,(function(){return o(this,(function(t){try{null===(e=b())||void 0===e||e.localStorage.clear()}catch(e){}return[2]}))}))},e}(),Ue=function(e){function i(){var t=null!==e&&e.apply(this,arguments)||this;return t.state={done:4},t}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i=this;return o(this,(function(n){return[2,new Promise((function(n,r){"undefined"==typeof XMLHttpRequest&&r(new Error("XHRTransport is not supported."));var o=new XMLHttpRequest;o.open("POST",e,!0),o.onreadystatechange=function(){if(o.readyState===i.state.done)try{var e=o.responseText,t=JSON.parse(e),s=i.buildResponse(t);n(s)}catch(e){r(e)}},o.setRequestHeader("Content-Type","application/json"),o.setRequestHeader("Accept","*/*"),o.send(JSON.stringify(t))}))]}))}))},i}(ue),De=function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return t(i,e),i.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var i=this;return o(this,(function(n){return[2,new Promise((function(n,r){var o=b();if(!(null==o?void 0:o.navigator.sendBeacon))throw new Error("SendBeaconTransport is not supported");try{var s=JSON.stringify(t);return n(o.navigator.sendBeacon(e,JSON.stringify(t))?i.buildResponse({code:200,events_ingested:t.events.length,payload_size_bytes:s.length,server_upload_time:Date.now()}):i.buildResponse({code:500}))}catch(e){r(e)}}))]}))}))},i}(ue),Ae=function(){return{cookieExpiration:365,cookieSameSite:"Lax",cookieSecure:!1,cookieStorage:new ae,cookieUpgrade:!0,disableCookies:!1,domain:"",sessionTimeout:18e5,trackingOptions:{deviceManufacturer:!0,deviceModel:!0,ipAddress:!0,language:!0,osName:!0,osVersion:!0,platform:!0},transportProvider:new de}},Ce=function(e){function n(t,n){var r,o,s,a,u,c,l,d,p,f=this,v=Ae();return(f=e.call(this,i(i({flushIntervalMillis:1e3,flushMaxRetries:5,flushQueueSize:30,transportProvider:v.transportProvider},n),{apiKey:t}))||this)._optOut=!1,f.cookieStorage=null!==(r=null==n?void 0:n.cookieStorage)&&void 0!==r?r:v.cookieStorage,f.deviceId=null==n?void 0:n.deviceId,f.lastEventId=null==n?void 0:n.lastEventId,f.lastEventTime=null==n?void 0:n.lastEventTime,f.optOut=Boolean(null==n?void 0:n.optOut),f.sessionId=null==n?void 0:n.sessionId,f.userId=null==n?void 0:n.userId,f.appVersion=null==n?void 0:n.appVersion,f.attribution=null==n?void 0:n.attribution,f.cookieExpiration=null!==(o=null==n?void 0:n.cookieExpiration)&&void 0!==o?o:v.cookieExpiration,f.cookieSameSite=null!==(s=null==n?void 0:n.cookieSameSite)&&void 0!==s?s:v.cookieSameSite,f.cookieSecure=null!==(a=null==n?void 0:n.cookieSecure)&&void 0!==a?a:v.cookieSecure,f.cookieUpgrade=null!==(u=null==n?void 0:n.cookieUpgrade)&&void 0!==u?u:v.cookieUpgrade,f.defaultTracking=null==n?void 0:n.defaultTracking,f.disableCookies=null!==(c=null==n?void 0:n.disableCookies)&&void 0!==c?c:v.disableCookies,f.defaultTracking=null==n?void 0:n.defaultTracking,f.domain=null!==(l=null==n?void 0:n.domain)&&void 0!==l?l:v.domain,f.partnerId=null==n?void 0:n.partnerId,f.sessionTimeout=null!==(d=null==n?void 0:n.sessionTimeout)&&void 0!==d?d:v.sessionTimeout,f.trackingOptions=null!==(p=null==n?void 0:n.trackingOptions)&&void 0!==p?p:v.trackingOptions,f}return t(n,e),Object.defineProperty(n.prototype,"deviceId",{get:function(){return this._deviceId},set:function(e){this._deviceId!==e&&(this._deviceId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"userId",{get:function(){return this._userId},set:function(e){this._userId!==e&&(this._userId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"sessionId",{get:function(){return this._sessionId},set:function(e){this._sessionId!==e&&(this._sessionId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"optOut",{get:function(){return this._optOut},set:function(e){this._optOut!==e&&(this._optOut=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"lastEventTime",{get:function(){return this._lastEventTime},set:function(e){this._lastEventTime!==e&&(this._lastEventTime=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"lastEventId",{get:function(){return this._lastEventId},set:function(e){this._lastEventId!==e&&(this._lastEventId=e,this.updateStorage())},enumerable:!1,configurable:!0}),n.prototype.updateStorage=function(){var e,t={deviceId:this._deviceId,userId:this._userId,sessionId:this._sessionId,optOut:this._optOut,lastEventTime:this._lastEventTime,lastEventId:this._lastEventId};null===(e=this.cookieStorage)||void 0===e||e.set(ce(this.apiKey),t)},n}(G),je=function(e,t){return r(void 0,void 0,void 0,(function(){var n,r,s,a,u,c,l,d,p,f,v,h,g,b,y;return o(this,(function(o){switch(o.label){case 0:return n=Ae(),r=null!==(b=null==t?void 0:t.deviceId)&&void 0!==b?b:se(),s=null==t?void 0:t.lastEventId,a=null==t?void 0:t.lastEventTime,u=null==t?void 0:t.optOut,c=null==t?void 0:t.sessionId,l=null==t?void 0:t.userId,d=null==t?void 0:t.cookieStorage,p=null==t?void 0:t.domain,f=Ce.bind,v=[void 0,e],h=[i({},t)],g={cookieStorage:d,deviceId:r,domain:p,lastEventId:s,lastEventTime:a,optOut:u,sessionId:c},[4,Ve(t)];case 1:return[2,new(f.apply(Ce,v.concat([i.apply(void 0,h.concat([(g.storageProvider=o.sent(),g.trackingOptions=i(i({},n.trackingOptions),null==t?void 0:t.trackingOptions),g.transportProvider=null!==(y=null==t?void 0:t.transportProvider)&&void 0!==y?y:ze(null==t?void 0:t.transport),g.userId=l,g)]))])))]}}))}))},Le=function(e,t){return void 0===t&&(t=Ae()),r(void 0,void 0,void 0,(function(){var n,r,s;return o(this,(function(o){switch(o.label){case 0:return n=i(i({},t),e),r=null==e?void 0:e.cookieStorage,(s=!r)?[3,2]:[4,r.isEnabled()];case 1:s=!o.sent(),o.label=2;case 2:return s?[2,Me(n)]:[2,r]}}))}))},Me=function(e){return r(void 0,void 0,void 0,(function(){var t,i;return o(this,(function(n){switch(n.label){case 0:return t=new le({domain:e.domain,expirationDays:e.cookieExpiration,sameSite:e.cookieSameSite,secure:e.cookieSecure}),(i=e.disableCookies)?[3,2]:[4,t.isEnabled()];case 1:i=!n.sent(),n.label=2;case 2:return i?[4,(t=new qe).isEnabled()]:[3,4];case 3:n.sent()||(t=new ae),n.label=4;case 4:return[2,t]}}))}))},Ve=function(e){return r(void 0,void 0,void 0,(function(){var t,i,n,r,a,u,c;return o(this,(function(o){switch(o.label){case 0:if(e&&Object.prototype.hasOwnProperty.call(e,"storageProvider")&&!e.storageProvider)return[3,9];o.label=1;case 1:o.trys.push([1,7,8,9]),t=s([null==e?void 0:e.storageProvider,new qe]),i=t.next(),o.label=2;case 2:return i.done?[3,6]:(n=i.value,(r=n)?[4,n.isEnabled()]:[3,4]);case 3:r=o.sent(),o.label=4;case 4:if(r)return[2,n];o.label=5;case 5:return i=t.next(),[3,2];case 6:return[3,9];case 7:return a=o.sent(),u={error:a},[3,9];case 8:try{i&&!i.done&&(c=t.return)&&c.call(t)}finally{if(u)throw u.error}return[7];case 9:return[2,void 0]}}))}))},ze=function(e){return e===g.XHR?new Ue:e===g.SendBeacon?new De:Ae().transportProvider},Fe=function(e,t){return r(void 0,void 0,void 0,(function(){var i,n,r,s,u,c,l,d,p,f,v;return o(this,(function(o){switch(o.label){case 0:return[4,Le(t)];case 1:return i=o.sent(),n=function(e){return"".concat(D.toLowerCase(),"_").concat(e.substring(0,6))}(e),[4,i.getRaw(n)];case 2:return(r=o.sent())?(null!==(v=t.cookieUpgrade)&&void 0!==v?v:Ae().cookieUpgrade)?[4,i.remove(n)]:[3,4]:[2,{optOut:!1}];case 3:o.sent(),o.label=4;case 4:return s=a(r.split("."),6),u=s[0],c=s[1],l=s[2],d=s[3],p=s[4],f=s[5],[2,{deviceId:u,userId:$e(c),sessionId:Be(d),lastEventId:Be(f),lastEventTime:Be(p),optOut:Boolean(l)}]}}))}))},Be=function(e){var t=parseInt(e,32);if(!isNaN(t))return t},$e=function(e){if(atob&&escape&&e)try{return decodeURIComponent(escape(atob(e)))}catch(e){return}},Qe=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[D,t,e.substring(0,i)].filter(Boolean).join("_")},Ke=function(e){var t=e.split(".");return t.length<=2?e:t.slice(t.length-2,t.length).join(".")},He=function(e,t,i){var r;e.referrer;var o=e.referring_domain,s=n(e,["referrer","referring_domain"]),a=t||{};a.referrer;var u=a.referring_domain,c=n(a,["referrer","referring_domain"]);if(e.referring_domain&&(null===(r=i.excludeReferrers)||void 0===r?void 0:r.includes(e.referring_domain)))return!1;var l=JSON.stringify(s)!==JSON.stringify(c),d=Ke(o||"")!==Ke(u||"");return!t||l||d},We=function(e,t){var n=i(i({},N),e),r=Object.entries(n).reduce((function(e,i){var n,r=a(i,2),o=r[0],s=r[1];return e.setOnce("initial_".concat(o),null!==(n=null!=s?s:t.initialEmptyValue)&&void 0!==n?n:"EMPTY"),s?e.set(o,s):e.unset(o)}),new M);return V(r)},Ze=function(e){var t={};for(var i in e){var n=e[i];n&&(t[i]=n)}return t},Ge=function(){for(var e,t=[],n=0;n=0;--r)i.push(t.slice(r).join("."));r=0,o.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(null!==(I=null!==(_=t.sessionId)&&void 0!==_?_:this.config.sessionId)&&void 0!==I?I:Date.now()),B=!0),($=Ie(t.instanceName)).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new ee).promise];case 11:return W.sent(),[4,this.add(new Ne).promise];case 12:return W.sent(),[4,this.add(new Ee).promise];case 13:return W.sent(),("boolean"==typeof(Z=this.config.defaultTracking)?Z:null==Z?void 0:Z.fileDownloads)?[4,this.add(gt()).promise]:[3,15];case 14:W.sent(),W.label=15;case 15:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add(ht()).promise]:[3,17];case 16:W.sent(),W.label=17;case 17:return(null===(E=this.config.attribution)||void 0===E?void 0:E.disabled)?[3,19]:(Q=function(){for(var e,t,n=this,s=[],u=0;uthis.config.sessionTimeout&&this.setSessionId(i),[2,e.prototype.process.call(this,t)]}))}))},n}(Q),mt=function(){var e=new yt;return{init:oe(e.init.bind(e),"init",ie(e),re(e,["config"])),add:oe(e.add.bind(e),"add",ie(e),re(e,["config.apiKey","timeline.plugins"])),remove:oe(e.remove.bind(e),"remove",ie(e),re(e,["config.apiKey","timeline.plugins"])),track:oe(e.track.bind(e),"track",ie(e),re(e,["config.apiKey","timeline.queue.length"])),logEvent:oe(e.logEvent.bind(e),"logEvent",ie(e),re(e,["config.apiKey","timeline.queue.length"])),identify:oe(e.identify.bind(e),"identify",ie(e),re(e,["config.apiKey","timeline.queue.length"])),groupIdentify:oe(e.groupIdentify.bind(e),"groupIdentify",ie(e),re(e,["config.apiKey","timeline.queue.length"])),setGroup:oe(e.setGroup.bind(e),"setGroup",ie(e),re(e,["config.apiKey","timeline.queue.length"])),revenue:oe(e.revenue.bind(e),"revenue",ie(e),re(e,["config.apiKey","timeline.queue.length"])),flush:oe(e.flush.bind(e),"flush",ie(e),re(e,["config.apiKey","timeline.queue.length"])),getUserId:oe(e.getUserId.bind(e),"getUserId",ie(e),re(e,["config","config.userId"])),setUserId:oe(e.setUserId.bind(e),"setUserId",ie(e),re(e,["config","config.userId"])),getDeviceId:oe(e.getDeviceId.bind(e),"getDeviceId",ie(e),re(e,["config","config.deviceId"])),setDeviceId:oe(e.setDeviceId.bind(e),"setDeviceId",ie(e),re(e,["config","config.deviceId"])),reset:oe(e.reset.bind(e),"reset",ie(e),re(e,["config","config.userId","config.deviceId"])),getSessionId:oe(e.getSessionId.bind(e),"getSessionId",ie(e),re(e,["config"])),setSessionId:oe(e.setSessionId.bind(e),"setSessionId",ie(e),re(e,["config"])),extendSession:oe(e.extendSession.bind(e),"extendSession",ie(e),re(e,["config"])),setOptOut:oe(e.setOptOut.bind(e),"setOptOut",ie(e),re(e,["config"])),setTransport:oe(e.setTransport.bind(e),"setTransport",ie(e),re(e,["config"]))}},wt=mt(),_t=wt.add,It=wt.extendSession,Et=wt.flush,kt=wt.getDeviceId,St=wt.getSessionId,Tt=wt.getUserId,Ot=wt.groupIdentify,xt=wt.identify,Pt=wt.init,Rt=wt.logEvent,Nt=wt.remove,qt=wt.reset,Ut=wt.revenue,Dt=wt.setDeviceId,At=wt.setGroup,Ct=wt.setOptOut,jt=wt.setSessionId,Lt=wt.setTransport,Mt=wt.setUserId,Vt=wt.track,zt=Object.freeze({__proto__:null,add:_t,extendSession:It,flush:Et,getDeviceId:kt,getSessionId:St,getUserId:Tt,groupIdentify:Ot,identify:xt,init:Pt,logEvent:Rt,remove:Nt,reset:qt,revenue:Ut,setDeviceId:Dt,setGroup:At,setOptOut:Ct,setSessionId:jt,setTransport:Lt,setUserId:Mt,track:Vt,Types:U,createInstance:mt,runQueuedFunctions:Se,Revenue:K,Identify:M});!function(){var e=b();if(e){var t=function(e){var t=mt(),i=b();return i&&i.amplitude&&i.amplitude._iq&&e&&(i.amplitude._iq[e]=t),t};if(e.amplitude=Object.assign(e.amplitude||{},zt,{createInstance:t}),e.amplitude.invoked){var i=e.amplitude._q;e.amplitude._q=[],Se(zt,i);for(var n=Object.keys(e.amplitude._iq)||[],r=0;r ```html diff --git a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js index 4a216eefc..abe75bd6d 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-uQFYsRWKr27i5VRoSShdW+SWnMC3RYcwlKIyk4cs8E1KrVgcdqeAI4Dca3cGejM+'; + as.integrity = 'sha384-juCBofnD0wr/Qoa2qChzdXfo79rKhQedVQ+CXUPUj203nMMvPVjVN7KGTw+ONROe'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.6-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.7-min.js.gz'; as.onload = function () { if (!window.amplitudeGTM.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/generated/amplitude-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-snippet.js index cac52bf19..57be95124 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-snippet.js @@ -49,10 +49,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-LxzsESYOYIQDLuCbztiSAfn/WnK7IQMVua4D9OvBkNgfqKHs8lZOfldylsVDG7m4'; + as.integrity = 'sha384-XaMbg2DwRNF/qnNtpb/cOoV+zkqyN+yl8FTT5d9XxigwSyhwjDlpSeZwbcBzun7j'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.6-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.7-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/package.json b/packages/marketing-analytics-browser/package.json index 070ca4b1c..f3f98143b 100644 --- a/packages/marketing-analytics-browser/package.json +++ b/packages/marketing-analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/marketing-analytics-browser", - "version": "1.0.6", + "version": "1.0.7", "description": "Official Amplitude SDK for Web and Marketing Analytics", "keywords": [ "analytics", @@ -43,12 +43,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.12.0", - "@amplitude/analytics-client-common": "^1.1.0", - "@amplitude/analytics-core": "^1.2.0", - "@amplitude/analytics-types": "^1.3.0", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.6", - "@amplitude/plugin-web-attribution-browser": "^1.0.6", + "@amplitude/analytics-browser": "^1.12.1", + "@amplitude/analytics-client-common": "^1.1.1", + "@amplitude/analytics-core": "^1.2.1", + "@amplitude/analytics-types": "^1.3.1", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.7", + "@amplitude/plugin-web-attribution-browser": "^1.0.7", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/marketing-analytics-browser/src/version.ts b/packages/marketing-analytics-browser/src/version.ts index 3f6f127aa..ee5d17a8f 100644 --- a/packages/marketing-analytics-browser/src/version.ts +++ b/packages/marketing-analytics-browser/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.0.6'; +export const VERSION = '1.0.7'; diff --git a/packages/plugin-page-view-tracking-browser/CHANGELOG.md b/packages/plugin-page-view-tracking-browser/CHANGELOG.md index 401e9f308..e9f365677 100644 --- a/packages/plugin-page-view-tracking-browser/CHANGELOG.md +++ b/packages/plugin-page-view-tracking-browser/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.7](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.6...@amplitude/plugin-page-view-tracking-browser@1.0.7) (2023-07-05) + +### Bug Fixes + +- allow plugins to teardown to remove listeners ([#463](https://github.com/amplitude/Amplitude-TypeScript/issues/463)) + ([1394ddb](https://github.com/amplitude/Amplitude-TypeScript/commit/1394ddb3775c7e5b1a3da87e6a4c973a586c6382)) +- handle concurrent history push state calls ([#466](https://github.com/amplitude/Amplitude-TypeScript/issues/466)) + ([4ebf96b](https://github.com/amplitude/Amplitude-TypeScript/commit/4ebf96b8c611cc0c766446b35e8a209bf5af580b)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.5...@amplitude/plugin-page-view-tracking-browser@1.0.6) (2023-06-26) **Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser diff --git a/packages/plugin-page-view-tracking-browser/package.json b/packages/plugin-page-view-tracking-browser/package.json index a2cd4b9a0..8bb17ae6c 100644 --- a/packages/plugin-page-view-tracking-browser/package.json +++ b/packages/plugin-page-view-tracking-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-page-view-tracking-browser", - "version": "1.0.6", + "version": "1.0.7", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -37,12 +37,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.1.0", - "@amplitude/analytics-types": "^1.3.0", + "@amplitude/analytics-client-common": "^1.1.1", + "@amplitude/analytics-types": "^1.3.1", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.12.0", + "@amplitude/analytics-browser": "^1.12.1", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 758270f9e..6411fb68e 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.1.0...@amplitude/plugin-session-replay-browser@0.1.1) (2023-07-05) + +### Bug Fixes + +- allow literal values for plugin type ([#468](https://github.com/amplitude/Amplitude-TypeScript/issues/468)) + ([603e3ef](https://github.com/amplitude/Amplitude-TypeScript/commit/603e3eff81a3d03082544541a673df955cf30118)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # 0.1.0 (2023-06-28) ### Features diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index d2635ef02..73130a832 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.1.0", + "version": "0.1.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -37,14 +37,14 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": ">=1 <3", - "@amplitude/analytics-types": ">=1 <3", + "@amplitude/analytics-client-common": "^1.1.1", + "@amplitude/analytics-types": "^1.3.1", "idb-keyval": "^6.2.1", "rrweb": "^2.0.0-alpha.4", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": ">=1 <3", + "@amplitude/analytics-browser": "^1.12.1", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", diff --git a/packages/plugin-web-attribution-browser/CHANGELOG.md b/packages/plugin-web-attribution-browser/CHANGELOG.md index a1bb02e33..5f9ab3bb8 100644 --- a/packages/plugin-web-attribution-browser/CHANGELOG.md +++ b/packages/plugin-web-attribution-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.7](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.6...@amplitude/plugin-web-attribution-browser@1.0.7) (2023-07-05) + +### Bug Fixes + +- missing core dependency for web attribution ([#462](https://github.com/amplitude/Amplitude-TypeScript/issues/462)) + ([902aa39](https://github.com/amplitude/Amplitude-TypeScript/commit/902aa39c8dcccfdc48f44312fe11a811a39cba10)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.5...@amplitude/plugin-web-attribution-browser@1.0.6) (2023-06-26) **Note:** Version bump only for package @amplitude/plugin-web-attribution-browser diff --git a/packages/plugin-web-attribution-browser/package.json b/packages/plugin-web-attribution-browser/package.json index c2d1c0d51..e1104e9ce 100644 --- a/packages/plugin-web-attribution-browser/package.json +++ b/packages/plugin-web-attribution-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-web-attribution-browser", - "version": "1.0.6", + "version": "1.0.7", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -37,13 +37,13 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.1.0", - "@amplitude/analytics-core": "^1.2.0", - "@amplitude/analytics-types": "^1.3.0", + "@amplitude/analytics-client-common": "^1.1.1", + "@amplitude/analytics-core": "^1.2.1", + "@amplitude/analytics-types": "^1.3.1", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.12.0", + "@amplitude/analytics-browser": "^1.12.1", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", From fadd7c0cd24f78c6dd317098fe6dfad951ce206e Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 6 Jul 2023 15:45:18 -0400 Subject: [PATCH 050/214] fix(plugins): update timing of sending batches of session replay events --- .../src/helpers.ts | 8 -- .../src/session-replay.ts | 40 +++++- .../src/typings/session-replay.ts | 3 + .../test/session-replay.test.ts | 135 +++++++++++++++--- 4 files changed, 155 insertions(+), 31 deletions(-) delete mode 100644 packages/plugin-session-replay-browser/src/helpers.ts diff --git a/packages/plugin-session-replay-browser/src/helpers.ts b/packages/plugin-session-replay-browser/src/helpers.ts deleted file mode 100644 index 67d59de84..000000000 --- a/packages/plugin-session-replay-browser/src/helpers.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const shouldSplitEventsList = (eventsList: string[], nextEventString: string, maxListSize: number): boolean => { - const sizeOfNextEvent = new Blob([nextEventString]).size; - const sizeOfEventsList = new Blob(eventsList).size; - if (sizeOfEventsList + sizeOfNextEvent >= maxListSize) { - return true; - } - return false; -}; diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 2808f23c9..5e75ba438 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -3,7 +3,6 @@ import { BrowserConfig, Event, Status } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import { pack, record } from 'rrweb'; import { DEFAULT_SESSION_END_EVENT, DEFAULT_SESSION_REPLAY_PROPERTY, DEFAULT_SESSION_START_EVENT } from './constants'; -import { shouldSplitEventsList } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from './messages'; import { Events, @@ -17,6 +16,8 @@ const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/tra const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; +const MIN_INTERVAL = 1 * 1000; // 1 second +const MAX_INTERVAL = 10 * 1000; // 10 seconds class SessionReplay implements SessionReplayEnrichmentPlugin { name = '@amplitude/plugin-session-replay-browser'; @@ -33,6 +34,8 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { queue: SessionReplayContext[] = []; stopRecordingEvents: ReturnType | null = null; maxPersistedEventsSize = MAX_EVENT_LIST_SIZE_IN_BYTES; + interval = MIN_INTERVAL; + timeSinceLastSend: number | null = null; async setup(config: BrowserConfig) { config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); @@ -47,8 +50,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { ...event.event_properties, [DEFAULT_SESSION_REPLAY_PROPERTY]: true, }; - if (event.event_type === DEFAULT_SESSION_START_EVENT && this.stopRecordingEvents) { - this.stopRecordingEvents(); + if (event.event_type === DEFAULT_SESSION_START_EVENT) { this.recordEvents(); } else if (event.event_type === DEFAULT_SESSION_END_EVENT) { if (event.session_id) { @@ -58,6 +60,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { sessionId: event.session_id, }); } + this.stopRecordingEvents && this.stopRecordingEvents(); this.events = []; this.currentSequenceId = 0; } @@ -88,8 +91,20 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { recordEvents() { this.stopRecordingEvents = record({ emit: (event) => { + console.log('this.events', this.events); const eventString = JSON.stringify(event); - const shouldSplit = shouldSplitEventsList(this.events, eventString, this.maxPersistedEventsSize); + // Send the first recorded event immediately + if (!this.events.length && this.currentSequenceId === 0) { + this.sendEventsList({ + events: [eventString], + sequenceId: this.currentSequenceId, + sessionId: this.config.sessionId as number, + }); + this.currentSequenceId++; + return; + } + + const shouldSplit = this.shouldSplitEventsList(eventString); if (shouldSplit) { this.sendEventsList({ events: this.events, @@ -103,9 +118,23 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { void this.storeEventsForSession(this.events, this.currentSequenceId); }, packFn: pack, + maskAllInputs: true, }); } + shouldSplitEventsList = (nextEventString: string): boolean => { + const sizeOfNextEvent = new Blob([nextEventString]).size; + const sizeOfEventsList = new Blob(this.events).size; + if (sizeOfEventsList + sizeOfNextEvent >= this.maxPersistedEventsSize) { + return true; + } + if (this.timeSinceLastSend !== null && Date.now() - this.timeSinceLastSend > this.interval) { + this.interval = Math.min(MAX_INTERVAL, this.interval + MIN_INTERVAL); + return true; + } + return false; + }; + sendEventsList({ events, sequenceId, sessionId }: { events: string[]; sequenceId: number; sessionId: number }) { this.addToQueue({ events, @@ -131,7 +160,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { tryable.forEach((context) => { this.queue = this.queue.concat(context); if (context.timeout === 0) { - this.schedule(this.config.flushIntervalMillis); + this.schedule(0); return; } @@ -286,6 +315,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { if (err) { this.config.loggerProvider.error(err); } else if (success) { + this.timeSinceLastSend = Date.now(); this.config.loggerProvider.log(success); } } diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index f0bd54a48..82bc720d3 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -26,11 +26,14 @@ export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { retryTimeout: number; events: Events; currentSequenceId: number; + interval: number; queue: SessionReplayContext[]; + timeSinceLastSend: number | null; stopRecordingEvents: ReturnType | null; maxPersistedEventsSize: number; emptyStoreAndReset: () => Promise; recordEvents: () => void; + shouldSplitEventsList: (nextEventString: string) => boolean; sendEventsList: ({ events, sequenceId, diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 0c1f3d4d4..2841b3d62 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -3,7 +3,6 @@ import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-commo import { BrowserConfig, LogLevel } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import * as RRWeb from 'rrweb'; -import { shouldSplitEventsList } from '../src/helpers'; import { SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from '../src/messages'; import { sessionReplayPlugin } from '../src/session-replay'; @@ -206,8 +205,6 @@ describe('SessionReplayPlugin', () => { test('should restart recording events when session_start fires', async () => { const sessionReplay = sessionReplayPlugin(); - const mockStopRecordingEvents = jest.fn(); - record.mockReturnValue(mockStopRecordingEvents); const mockGetResolution = Promise.resolve({}); get.mockReturnValueOnce(mockGetResolution); await sessionReplay.setup(mockConfig); @@ -217,12 +214,13 @@ describe('SessionReplayPlugin', () => { event_type: 'session_start', }; await sessionReplay.execute(event); - expect(mockStopRecordingEvents).toHaveBeenCalledTimes(1); expect(record).toHaveBeenCalledTimes(2); }); - test('should send the current events list when session_end fires', async () => { + test('should send the current events list when session_end fires and stop recording events', async () => { const sessionReplay = sessionReplayPlugin(); + const mockStopRecordingEvents = jest.fn(); + sessionReplay.stopRecordingEvents = mockStopRecordingEvents; sessionReplay.config = mockConfig; const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); @@ -232,6 +230,7 @@ describe('SessionReplayPlugin', () => { }; sessionReplay.events = [mockEventString]; await sessionReplay.execute(event); + expect(mockStopRecordingEvents).toHaveBeenCalledTimes(1); jest.runAllTimers(); expect(send).toHaveBeenCalledTimes(1); @@ -247,24 +246,78 @@ describe('SessionReplayPlugin', () => { }); describe('recordEvents', () => { + test('should send the first emitted event immediately', () => { + const sessionReplay = sessionReplayPlugin(); + const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); + sessionReplay.config = mockConfig; + sessionReplay.recordEvents(); + expect(sessionReplay.events).toEqual([]); + const recordArg = record.mock.calls[0][0]; + recordArg?.emit && recordArg?.emit(mockEvent); + expect(sessionReplay.events).toEqual([]); + jest.runAllTimers(); + // Should not store events in IDB + expect(update).not.toHaveBeenCalled(); + expect(send).toHaveBeenCalledTimes(1); + expect(send.mock.calls[0][0]).toEqual({ + events: [mockEventString], + sequenceId: 0, + attempts: 1, + timeout: 0, + sessionId: 123, + }); + }); + test('should store events in class and in IDB', () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; sessionReplay.recordEvents(); expect(sessionReplay.events).toEqual([]); const recordArg = record.mock.calls[0][0]; + // Emit first event, which gets sent immediately + recordArg?.emit && recordArg?.emit(mockEvent); + // Emit second event, which is stored in class and IDB recordArg?.emit && recordArg?.emit(mockEvent); expect(sessionReplay.events).toEqual([mockEventString]); expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1]({})).toEqual({ 123: { events: [mockEventString], - sequenceId: 0, + sequenceId: 1, }, }); }); - test('should split the events list and send', () => { + test('should split the events list at an increasing interval and send', () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + sessionReplay.recordEvents(); + sessionReplay.timeSinceLastSend = 1; + const dateNowMock = jest.spyOn(Date, 'now').mockReturnValue(1); + const sendEventsList = jest.spyOn(sessionReplay, 'sendEventsList'); + const recordArg = record.mock.calls[0][0]; + // Emit first event, which gets sent immediately + recordArg?.emit && recordArg?.emit(mockEvent); + expect(sendEventsList).toHaveBeenCalledTimes(1); + sendEventsList.mockClear(); + // Emit second event, which is not sent immediately + console.log('before second event'); + recordArg?.emit && recordArg?.emit(mockEvent); + expect(sendEventsList).toHaveBeenCalledTimes(0); + // Emit third event and advance timers to interval + dateNowMock.mockReturnValue(1002); + recordArg?.emit && recordArg?.emit(mockEvent); + expect(sendEventsList).toHaveBeenCalledTimes(1); + expect(sendEventsList).toHaveBeenCalledWith({ + events: [mockEventString], + sequenceId: 1, + sessionId: 123, + }); + expect(sessionReplay.events).toEqual([mockEventString]); + dateNowMock.mockClear(); + }); + + test('should split the events list at max size and send', () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; sessionReplay.maxPersistedEventsSize = 20; @@ -309,6 +362,7 @@ describe('SessionReplayPlugin', () => { }; sessionReplay.addToQueue(context); expect(schedule).toHaveBeenCalledTimes(1); + expect(schedule).toHaveBeenCalledWith(0); expect(context.attempts).toBe(1); }); @@ -769,17 +823,62 @@ describe('SessionReplayPlugin', () => { }); describe('shouldSplitEventsList', () => { - test('shouldSplitEventsList should return true if size of events list plus size of next event is over the max size', () => { - const eventsList = ['#'.repeat(20)]; - const nextEvent = 'a'; - const result = shouldSplitEventsList(eventsList, nextEvent, 20); - expect(result).toBe(true); - }); - test('shouldSplitEventsList should return false if size of events list plus size of next event is under the max size', () => { - const eventsList = ['#'.repeat(20)]; - const nextEvent = 'a'; - const result = shouldSplitEventsList(eventsList, nextEvent, 22); - expect(result).toBe(false); + describe('event list size', () => { + test('should return true if size of events list plus size of next event is over the max size', () => { + const sessionReplay = sessionReplayPlugin(); + const eventsList = ['#'.repeat(20)]; + sessionReplay.events = eventsList; + sessionReplay.maxPersistedEventsSize = 20; + const nextEvent = 'a'; + const result = sessionReplay.shouldSplitEventsList(nextEvent); + expect(result).toBe(true); + }); + test('should return false if size of events list plus size of next event is under the max size', () => { + const sessionReplay = sessionReplayPlugin(); + const eventsList = ['#'.repeat(20)]; + sessionReplay.events = eventsList; + sessionReplay.maxPersistedEventsSize = 22; + const nextEvent = 'a'; + const result = sessionReplay.shouldSplitEventsList(nextEvent); + expect(result).toBe(false); + }); + }); + describe('interval', () => { + test('should return false if timeSinceLastSend is null', () => { + const sessionReplay = sessionReplayPlugin(); + const nextEvent = 'a'; + const result = sessionReplay.shouldSplitEventsList(nextEvent); + expect(result).toBe(false); + }); + test('should return false if it has not been long enough since last send', () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.timeSinceLastSend = 1; + jest.spyOn(Date, 'now').mockReturnValue(2); + const nextEvent = 'a'; + const result = sessionReplay.shouldSplitEventsList(nextEvent); + expect(result).toBe(false); + }); + test('should return true if it has been long enough since last send', () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.timeSinceLastSend = 1; + jest.spyOn(Date, 'now').mockReturnValue(1002); + const nextEvent = 'a'; + const result = sessionReplay.shouldSplitEventsList(nextEvent); + expect(result).toBe(true); + }); + test('should increase interval incrementally', () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.timeSinceLastSend = 1; + jest.spyOn(Date, 'now').mockReturnValue(1000000); + const nextEvent = 'a'; + for (let i = 1; i <= 10; i++) { + expect(sessionReplay.interval).toEqual(1000 * i); + sessionReplay.shouldSplitEventsList(nextEvent); + } + // Call one more time to go over the 10 max + sessionReplay.shouldSplitEventsList(nextEvent); + expect(sessionReplay.interval).toEqual(10000); + }); }); }); }); From fc263ed34a955f0bc987cf7281a374065f95045c Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 6 Jul 2023 15:54:44 -0400 Subject: [PATCH 051/214] fix(plugins): remove console log --- packages/plugin-session-replay-browser/src/session-replay.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 5e75ba438..aa747c57f 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -91,7 +91,6 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { recordEvents() { this.stopRecordingEvents = record({ emit: (event) => { - console.log('this.events', this.events); const eventString = JSON.stringify(event); // Send the first recorded event immediately if (!this.events.length && this.currentSequenceId === 0) { From 721a3997673d700b6bda9302b076be0f3fab7c09 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 6 Jul 2023 16:06:28 -0400 Subject: [PATCH 052/214] fix(plugins): need to send first two events immediately for replayer to work --- .../src/session-replay.ts | 7 +++-- .../test/session-replay.test.ts | 29 ++++++++++--------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index aa747c57f..650345273 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -92,13 +92,14 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { this.stopRecordingEvents = record({ emit: (event) => { const eventString = JSON.stringify(event); - // Send the first recorded event immediately - if (!this.events.length && this.currentSequenceId === 0) { + // Send the first two recorded events immediately + if (this.events.length === 1 && this.currentSequenceId === 0) { this.sendEventsList({ - events: [eventString], + events: this.events.concat(eventString), sequenceId: this.currentSequenceId, sessionId: this.config.sessionId as number, }); + this.events = []; this.currentSequenceId++; return; } diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 2841b3d62..792df9fa1 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -246,7 +246,7 @@ describe('SessionReplayPlugin', () => { }); describe('recordEvents', () => { - test('should send the first emitted event immediately', () => { + test('should send the first two emitted events immediately', () => { const sessionReplay = sessionReplayPlugin(); const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); sessionReplay.config = mockConfig; @@ -254,13 +254,12 @@ describe('SessionReplayPlugin', () => { expect(sessionReplay.events).toEqual([]); const recordArg = record.mock.calls[0][0]; recordArg?.emit && recordArg?.emit(mockEvent); + recordArg?.emit && recordArg?.emit(mockEvent); expect(sessionReplay.events).toEqual([]); jest.runAllTimers(); - // Should not store events in IDB - expect(update).not.toHaveBeenCalled(); expect(send).toHaveBeenCalledTimes(1); expect(send.mock.calls[0][0]).toEqual({ - events: [mockEventString], + events: [mockEventString, mockEventString], sequenceId: 0, attempts: 1, timeout: 0, @@ -274,13 +273,14 @@ describe('SessionReplayPlugin', () => { sessionReplay.recordEvents(); expect(sessionReplay.events).toEqual([]); const recordArg = record.mock.calls[0][0]; - // Emit first event, which gets sent immediately + // Emit first two events, which get sent immediately recordArg?.emit && recordArg?.emit(mockEvent); - // Emit second event, which is stored in class and IDB + recordArg?.emit && recordArg?.emit(mockEvent); + // Emit third event, which is stored in class and IDB recordArg?.emit && recordArg?.emit(mockEvent); expect(sessionReplay.events).toEqual([mockEventString]); - expect(update).toHaveBeenCalledTimes(1); - expect(update.mock.calls[0][1]({})).toEqual({ + expect(update).toHaveBeenCalledTimes(2); + expect(update.mock.calls[1][1]({})).toEqual({ 123: { events: [mockEventString], sequenceId: 1, @@ -296,15 +296,15 @@ describe('SessionReplayPlugin', () => { const dateNowMock = jest.spyOn(Date, 'now').mockReturnValue(1); const sendEventsList = jest.spyOn(sessionReplay, 'sendEventsList'); const recordArg = record.mock.calls[0][0]; - // Emit first event, which gets sent immediately + // Emit first two events, which get sent immediately + recordArg?.emit && recordArg?.emit(mockEvent); recordArg?.emit && recordArg?.emit(mockEvent); expect(sendEventsList).toHaveBeenCalledTimes(1); sendEventsList.mockClear(); - // Emit second event, which is not sent immediately - console.log('before second event'); + // Emit third event, which is not sent immediately recordArg?.emit && recordArg?.emit(mockEvent); expect(sendEventsList).toHaveBeenCalledTimes(0); - // Emit third event and advance timers to interval + // Emit fourth event and advance timers to interval dateNowMock.mockReturnValue(1002); recordArg?.emit && recordArg?.emit(mockEvent); expect(sendEventsList).toHaveBeenCalledTimes(1); @@ -321,6 +321,7 @@ describe('SessionReplayPlugin', () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; sessionReplay.maxPersistedEventsSize = 20; + sessionReplay.currentSequenceId = 1; const events = ['#'.repeat(20)]; sessionReplay.events = events; // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -333,7 +334,7 @@ describe('SessionReplayPlugin', () => { expect(sendEventsListMock).toHaveBeenCalledTimes(1); expect(sendEventsListMock).toHaveBeenCalledWith({ events, - sequenceId: 0, + sequenceId: 1, sessionId: 123, }); @@ -342,7 +343,7 @@ describe('SessionReplayPlugin', () => { expect(update.mock.calls[0][1]({})).toEqual({ 123: { events: [mockEventString], - sequenceId: 1, + sequenceId: 2, }, }); }); From 59802fbc0c949373a2fd3566ed3db6a871e442a4 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Mon, 10 Jul 2023 09:41:57 -0400 Subject: [PATCH 053/214] fix(plugins): pr feedback --- .../plugin-session-replay-browser/package.json | 5 ++--- .../src/session-replay.ts | 12 +++++++++--- .../src/typings/session-replay.ts | 2 +- .../test/session-replay.test.ts | 14 +++++++++----- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 73130a832..a763c83c4 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -37,14 +37,13 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.1.1", - "@amplitude/analytics-types": "^1.3.1", + "@amplitude/analytics-types": ">=1 <3", "idb-keyval": "^6.2.1", "rrweb": "^2.0.0-alpha.4", "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-browser": "^1.12.1", + "@amplitude/analytics-client-common": ">=1 <3", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 650345273..333d6529a 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -35,7 +35,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { stopRecordingEvents: ReturnType | null = null; maxPersistedEventsSize = MAX_EVENT_LIST_SIZE_IN_BYTES; interval = MIN_INTERVAL; - timeSinceLastSend: number | null = null; + timeAtLastSend: number | null = null; async setup(config: BrowserConfig) { config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); @@ -122,13 +122,19 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { }); } + /** + * Determines whether to send the events list to the backend and start a new + * empty events list, based on the size of the list as well as the last time sent + * @param nextEventString + * @returns boolean + */ shouldSplitEventsList = (nextEventString: string): boolean => { const sizeOfNextEvent = new Blob([nextEventString]).size; const sizeOfEventsList = new Blob(this.events).size; if (sizeOfEventsList + sizeOfNextEvent >= this.maxPersistedEventsSize) { return true; } - if (this.timeSinceLastSend !== null && Date.now() - this.timeSinceLastSend > this.interval) { + if (this.timeAtLastSend !== null && Date.now() - this.timeAtLastSend > this.interval) { this.interval = Math.min(MAX_INTERVAL, this.interval + MIN_INTERVAL); return true; } @@ -315,7 +321,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { if (err) { this.config.loggerProvider.error(err); } else if (success) { - this.timeSinceLastSend = Date.now(); + this.timeAtLastSend = Date.now(); this.config.loggerProvider.log(success); } } diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index 82bc720d3..0085e319a 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -28,7 +28,7 @@ export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { currentSequenceId: number; interval: number; queue: SessionReplayContext[]; - timeSinceLastSend: number | null; + timeAtLastSend: number | null; stopRecordingEvents: ReturnType | null; maxPersistedEventsSize: number; emptyStoreAndReset: () => Promise; diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 792df9fa1..03b044688 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -251,10 +251,14 @@ describe('SessionReplayPlugin', () => { const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); sessionReplay.config = mockConfig; sessionReplay.recordEvents(); + // Confirm that no events have been set in events list yet expect(sessionReplay.events).toEqual([]); const recordArg = record.mock.calls[0][0]; + // Emit two events manually (replicating what rrweb would do itself) recordArg?.emit && recordArg?.emit(mockEvent); recordArg?.emit && recordArg?.emit(mockEvent); + // Confirm that there are still no events in the events list + // (because they are sent immediately instead of stored) expect(sessionReplay.events).toEqual([]); jest.runAllTimers(); expect(send).toHaveBeenCalledTimes(1); @@ -292,7 +296,7 @@ describe('SessionReplayPlugin', () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; sessionReplay.recordEvents(); - sessionReplay.timeSinceLastSend = 1; + sessionReplay.timeAtLastSend = 1; const dateNowMock = jest.spyOn(Date, 'now').mockReturnValue(1); const sendEventsList = jest.spyOn(sessionReplay, 'sendEventsList'); const recordArg = record.mock.calls[0][0]; @@ -845,7 +849,7 @@ describe('SessionReplayPlugin', () => { }); }); describe('interval', () => { - test('should return false if timeSinceLastSend is null', () => { + test('should return false if timeAtLastSend is null', () => { const sessionReplay = sessionReplayPlugin(); const nextEvent = 'a'; const result = sessionReplay.shouldSplitEventsList(nextEvent); @@ -853,7 +857,7 @@ describe('SessionReplayPlugin', () => { }); test('should return false if it has not been long enough since last send', () => { const sessionReplay = sessionReplayPlugin(); - sessionReplay.timeSinceLastSend = 1; + sessionReplay.timeAtLastSend = 1; jest.spyOn(Date, 'now').mockReturnValue(2); const nextEvent = 'a'; const result = sessionReplay.shouldSplitEventsList(nextEvent); @@ -861,7 +865,7 @@ describe('SessionReplayPlugin', () => { }); test('should return true if it has been long enough since last send', () => { const sessionReplay = sessionReplayPlugin(); - sessionReplay.timeSinceLastSend = 1; + sessionReplay.timeAtLastSend = 1; jest.spyOn(Date, 'now').mockReturnValue(1002); const nextEvent = 'a'; const result = sessionReplay.shouldSplitEventsList(nextEvent); @@ -869,7 +873,7 @@ describe('SessionReplayPlugin', () => { }); test('should increase interval incrementally', () => { const sessionReplay = sessionReplayPlugin(); - sessionReplay.timeSinceLastSend = 1; + sessionReplay.timeAtLastSend = 1; jest.spyOn(Date, 'now').mockReturnValue(1000000); const nextEvent = 'a'; for (let i = 1; i <= 10; i++) { From ee064fe94bd1f5c24f106c70b14cbfca2cda1364 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Tue, 11 Jul 2023 21:35:49 +0000 Subject: [PATCH 054/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.1.2 --- .../plugin-session-replay-browser/CHANGELOG.md | 18 ++++++++++++++++++ .../plugin-session-replay-browser/package.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 6411fb68e..9f302ee2f 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,24 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.1.1...@amplitude/plugin-session-replay-browser@0.1.2) (2023-07-11) + +### Bug Fixes + +- **plugins:** need to send first two events immediately for replayer to work + ([721a399](https://github.com/amplitude/Amplitude-TypeScript/commit/721a3997673d700b6bda9302b076be0f3fab7c09)) +- **plugins:** pr feedback + ([59802fb](https://github.com/amplitude/Amplitude-TypeScript/commit/59802fbc0c949373a2fd3566ed3db6a871e442a4)) +- **plugins:** remove console log + ([fc263ed](https://github.com/amplitude/Amplitude-TypeScript/commit/fc263ed34a955f0bc987cf7281a374065f95045c)) +- **plugins:** update timing of sending batches of session replay events + ([fadd7c0](https://github.com/amplitude/Amplitude-TypeScript/commit/fadd7c0cd24f78c6dd317098fe6dfad951ce206e)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.1.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.1.0...@amplitude/plugin-session-replay-browser@0.1.1) (2023-07-05) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index a763c83c4..1f21c59a1 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.1.1", + "version": "0.1.2", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 1a55aeede742f688f87f128a13cf41f91bda5224 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 13 Jul 2023 09:29:38 -0400 Subject: [PATCH 055/214] fix(plugins): change timing of record upon initialization for session replay plugin --- packages/plugin-session-replay-browser/src/session-replay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 333d6529a..a5a6669ab 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -84,8 +84,8 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { const currentSessionStoredEvents = this.config.sessionId && storedReplaySessions[this.config.sessionId]; this.currentSequenceId = currentSessionStoredEvents ? currentSessionStoredEvents.sequenceId + 1 : 0; void this.storeEventsForSession([], this.currentSequenceId); - this.recordEvents(); } + this.recordEvents(); } recordEvents() { From 708d4b94d0d8714b35fe3984e604327e34b446f0 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 13 Jul 2023 09:32:15 -0400 Subject: [PATCH 056/214] fix(plugins): should only initiate recording on session start if recording is not initiated --- packages/plugin-session-replay-browser/src/session-replay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index a5a6669ab..99c2913db 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -50,7 +50,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { ...event.event_properties, [DEFAULT_SESSION_REPLAY_PROPERTY]: true, }; - if (event.event_type === DEFAULT_SESSION_START_EVENT) { + if (event.event_type === DEFAULT_SESSION_START_EVENT && !this.stopRecordingEvents) { this.recordEvents(); } else if (event.event_type === DEFAULT_SESSION_END_EVENT) { if (event.session_id) { From 392e3d9b080374d805e32d9e8a99f92175ea2218 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 13 Jul 2023 17:37:34 +0000 Subject: [PATCH 057/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.1.3 --- .../plugin-session-replay-browser/CHANGELOG.md | 14 ++++++++++++++ .../plugin-session-replay-browser/package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 9f302ee2f..dacc6d34f 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.1.2...@amplitude/plugin-session-replay-browser@0.1.3) (2023-07-13) + +### Bug Fixes + +- **plugins:** change timing of record upon initialization for session replay plugin + ([1a55aee](https://github.com/amplitude/Amplitude-TypeScript/commit/1a55aeede742f688f87f128a13cf41f91bda5224)) +- **plugins:** should only initiate recording on session start if recording is not initiated + ([708d4b9](https://github.com/amplitude/Amplitude-TypeScript/commit/708d4b94d0d8714b35fe3984e604327e34b446f0)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.1.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.1.1...@amplitude/plugin-session-replay-browser@0.1.2) (2023-07-11) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 1f21c59a1..d7e7203c0 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.1.2", + "version": "0.1.3", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From d2e9a0cc7c455dcdf13ea2fa303d2fbd50911536 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Wed, 19 Jul 2023 09:54:48 -0400 Subject: [PATCH 058/214] feat(plugins): solve timing issues with multiple tabs --- .../src/session-replay.ts | 90 +++++++++++++------ .../src/typings/session-replay.ts | 2 +- 2 files changed, 62 insertions(+), 30 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 99c2913db..a631c4255 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -1,7 +1,8 @@ +import { getGlobalScope } from '@amplitude/analytics-client-common'; import { AMPLITUDE_PREFIX, BaseTransport } from '@amplitude/analytics-core'; import { BrowserConfig, Event, Status } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; -import { pack, record } from 'rrweb'; +import { record } from 'rrweb'; import { DEFAULT_SESSION_END_EVENT, DEFAULT_SESSION_REPLAY_PROPERTY, DEFAULT_SESSION_START_EVENT } from './constants'; import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from './messages'; import { @@ -16,7 +17,7 @@ const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/tra const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; -const MIN_INTERVAL = 1 * 1000; // 1 second +// const MIN_INTERVAL = 1 * 1000; // 1 second const MAX_INTERVAL = 10 * 1000; // 10 seconds class SessionReplay implements SessionReplayEnrichmentPlugin { @@ -34,7 +35,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { queue: SessionReplayContext[] = []; stopRecordingEvents: ReturnType | null = null; maxPersistedEventsSize = MAX_EVENT_LIST_SIZE_IN_BYTES; - interval = MIN_INTERVAL; + interval = MAX_INTERVAL; timeAtLastSend: number | null = null; async setup(config: BrowserConfig) { @@ -43,6 +44,37 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { this.config = config; this.storageKey = `${STORAGE_PREFIX}_${this.config.apiKey.substring(0, 10)}`; void this.emptyStoreAndReset(); + this.timeAtLastSend = Date.now(); + + const GlobalScope = getGlobalScope(); + if (GlobalScope && GlobalScope.window) { + GlobalScope.window.addEventListener('blur', () => { + // console.log('blur - sending') + console.log('blur - recording stopped', this.stopRecordingEvents); + this.stopRecordingEvents && this.stopRecordingEvents(); + this.stopRecordingEvents = null; + }); + GlobalScope.window.addEventListener('focus', () => { + console.log( + 'focus', + 'this.stopRecordingEvents', + this.stopRecordingEvents, + 'config.sessionId', + config.sessionId, + ); + void this.getAllSessionEventsFromStore().then((storedReplaySessions) => { + console.log('storedReplaySessions', storedReplaySessions); + if (storedReplaySessions && storedReplaySessions[config.sessionId as number]) { + this.events = storedReplaySessions[config.sessionId as number].events; + this.currentSequenceId = storedReplaySessions[config.sessionId as number].sequenceId; + this.timeAtLastSend = Date.now(); + } + if (!this.stopRecordingEvents) { + this.recordEvents(); + } + }); + }); + } } async execute(event: Event) { @@ -59,8 +91,10 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { sequenceId: this.currentSequenceId, sessionId: event.session_id, }); + void this.removeSessionEventsStore(event.session_id); } this.stopRecordingEvents && this.stopRecordingEvents(); + this.stopRecordingEvents = null; this.events = []; this.currentSequenceId = 0; } @@ -70,9 +104,17 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { async emptyStoreAndReset() { const storedReplaySessions = await this.getAllSessionEventsFromStore(); if (storedReplaySessions) { + console.log('storedReplaySessions', storedReplaySessions); for (const sessionId in storedReplaySessions) { const storedReplayEvents = storedReplaySessions[sessionId]; if (storedReplayEvents.events.length) { + console.log( + 'sending from empty store', + 'events', + storedReplayEvents.events.length, + 'storedReplayEvents.sequenceId', + storedReplayEvents.sequenceId, + ); this.sendEventsList({ events: storedReplayEvents.events, sequenceId: storedReplayEvents.sequenceId, @@ -83,26 +125,18 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { this.events = []; const currentSessionStoredEvents = this.config.sessionId && storedReplaySessions[this.config.sessionId]; this.currentSequenceId = currentSessionStoredEvents ? currentSessionStoredEvents.sequenceId + 1 : 0; - void this.storeEventsForSession([], this.currentSequenceId); + void this.storeEventsForSession([], this.currentSequenceId, this.config.sessionId as number); + } + if (!this.stopRecordingEvents) { + this.recordEvents(); } - this.recordEvents(); } recordEvents() { this.stopRecordingEvents = record({ emit: (event) => { + console.log('recording event', event); const eventString = JSON.stringify(event); - // Send the first two recorded events immediately - if (this.events.length === 1 && this.currentSequenceId === 0) { - this.sendEventsList({ - events: this.events.concat(eventString), - sequenceId: this.currentSequenceId, - sessionId: this.config.sessionId as number, - }); - this.events = []; - this.currentSequenceId++; - return; - } const shouldSplit = this.shouldSplitEventsList(eventString); if (shouldSplit) { @@ -115,9 +149,9 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { this.currentSequenceId++; } this.events.push(eventString); - void this.storeEventsForSession(this.events, this.currentSequenceId); + void this.storeEventsForSession(this.events, this.currentSequenceId, this.config.sessionId as number); }, - packFn: pack, + // packFn: pack, maskAllInputs: true, }); } @@ -134,8 +168,9 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { if (sizeOfEventsList + sizeOfNextEvent >= this.maxPersistedEventsSize) { return true; } - if (this.timeAtLastSend !== null && Date.now() - this.timeAtLastSend > this.interval) { - this.interval = Math.min(MAX_INTERVAL, this.interval + MIN_INTERVAL); + if (this.timeAtLastSend !== null && Date.now() - this.timeAtLastSend > this.interval && this.events.length) { + // this.interval = Math.min(MAX_INTERVAL, this.interval + MIN_INTERVAL); + this.timeAtLastSend = Date.now(); return true; } return false; @@ -277,17 +312,15 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { return undefined; } - async storeEventsForSession(events: Events, sequenceId: number) { + async storeEventsForSession(events: Events, sequenceId: number, sessionId: number) { try { await IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore | undefined): IDBStore => { return { ...sessionMap, - ...(this.config.sessionId && { - [this.config.sessionId]: { - events: events, - sequenceId, - }, - }), + [sessionId]: { + events: events, + sequenceId, + }, }; }); } catch (e) { @@ -317,11 +350,10 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { success?: string; removeEvents?: boolean; }) { - removeEvents && context.sessionId && this.removeSessionEventsStore(context.sessionId); + removeEvents && context.sessionId && this.storeEventsForSession([], context.sequenceId, context.sessionId); if (err) { this.config.loggerProvider.error(err); } else if (success) { - this.timeAtLastSend = Date.now(); this.config.loggerProvider.log(success); } } diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index 0085e319a..1fa822259 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -59,7 +59,7 @@ export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { removeEvents?: boolean | undefined; }): void; getAllSessionEventsFromStore: () => Promise; - storeEventsForSession: (events: Events, sequenceId: number) => Promise; + storeEventsForSession: (events: Events, sequenceId: number, sessionId: number) => Promise; removeSessionEventsStore: (sessionId: number) => Promise; } From b73a4d81a74ed7adcdbd3b16ed2b4eab8d64bd05 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 20 Jul 2023 12:34:32 -0400 Subject: [PATCH 059/214] test(plugins): update tests for session replay --- .../src/session-replay.ts | 141 +++-- .../src/typings/session-replay.ts | 19 +- .../test/session-replay.test.ts | 580 ++++++++++++++---- 3 files changed, 554 insertions(+), 186 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index a631c4255..ffcc7e669 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -2,12 +2,13 @@ import { getGlobalScope } from '@amplitude/analytics-client-common'; import { AMPLITUDE_PREFIX, BaseTransport } from '@amplitude/analytics-core'; import { BrowserConfig, Event, Status } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; -import { record } from 'rrweb'; +import { pack, record } from 'rrweb'; import { DEFAULT_SESSION_END_EVENT, DEFAULT_SESSION_REPLAY_PROPERTY, DEFAULT_SESSION_START_EVENT } from './constants'; import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from './messages'; import { Events, IDBStore, + RecordingStatus, SessionReplayContext, SessionReplayEnrichmentPlugin, SessionReplayPlugin, @@ -17,7 +18,7 @@ const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/tra const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; -// const MIN_INTERVAL = 1 * 1000; // 1 second +const MIN_INTERVAL = 1 * 1000; // 1 second const MAX_INTERVAL = 10 * 1000; // 10 seconds class SessionReplay implements SessionReplayEnrichmentPlugin { @@ -35,7 +36,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { queue: SessionReplayContext[] = []; stopRecordingEvents: ReturnType | null = null; maxPersistedEventsSize = MAX_EVENT_LIST_SIZE_IN_BYTES; - interval = MAX_INTERVAL; + interval = MIN_INTERVAL; timeAtLastSend: number | null = null; async setup(config: BrowserConfig) { @@ -43,36 +44,16 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { this.config = config; this.storageKey = `${STORAGE_PREFIX}_${this.config.apiKey.substring(0, 10)}`; - void this.emptyStoreAndReset(); - this.timeAtLastSend = Date.now(); + await this.initialize(true); const GlobalScope = getGlobalScope(); if (GlobalScope && GlobalScope.window) { GlobalScope.window.addEventListener('blur', () => { - // console.log('blur - sending') - console.log('blur - recording stopped', this.stopRecordingEvents); this.stopRecordingEvents && this.stopRecordingEvents(); this.stopRecordingEvents = null; }); GlobalScope.window.addEventListener('focus', () => { - console.log( - 'focus', - 'this.stopRecordingEvents', - this.stopRecordingEvents, - 'config.sessionId', - config.sessionId, - ); - void this.getAllSessionEventsFromStore().then((storedReplaySessions) => { - console.log('storedReplaySessions', storedReplaySessions); - if (storedReplaySessions && storedReplaySessions[config.sessionId as number]) { - this.events = storedReplaySessions[config.sessionId as number].events; - this.currentSequenceId = storedReplaySessions[config.sessionId as number].sequenceId; - this.timeAtLastSend = Date.now(); - } - if (!this.stopRecordingEvents) { - this.recordEvents(); - } - }); + void this.initialize(); }); } } @@ -91,7 +72,6 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { sequenceId: this.currentSequenceId, sessionId: event.session_id, }); - void this.removeSessionEventsStore(event.session_id); } this.stopRecordingEvents && this.stopRecordingEvents(); this.stopRecordingEvents = null; @@ -101,41 +81,57 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { return Promise.resolve(event); } - async emptyStoreAndReset() { + async initialize(shouldSendStoredEvents = false) { + this.timeAtLastSend = Date.now(); // Initialize this so we have a point of comparison when events are recorded + if (!this.config.sessionId) { + return; + } const storedReplaySessions = await this.getAllSessionEventsFromStore(); - if (storedReplaySessions) { - console.log('storedReplaySessions', storedReplaySessions); - for (const sessionId in storedReplaySessions) { - const storedReplayEvents = storedReplaySessions[sessionId]; - if (storedReplayEvents.events.length) { - console.log( - 'sending from empty store', - 'events', - storedReplayEvents.events.length, - 'storedReplayEvents.sequenceId', - storedReplayEvents.sequenceId, - ); - this.sendEventsList({ - events: storedReplayEvents.events, - sequenceId: storedReplayEvents.sequenceId, - sessionId: parseInt(sessionId, 10), - }); - } + const storedSequencesForSession = storedReplaySessions && storedReplaySessions[this.config.sessionId]; + if (storedReplaySessions && storedSequencesForSession && storedSequencesForSession.sessionSequences) { + const storedSeqId = storedSequencesForSession.currentSequenceId; + const lastSequence = storedSequencesForSession.sessionSequences[storedSeqId]; + if (lastSequence.status !== RecordingStatus.RECORDING) { + this.currentSequenceId = storedSeqId + 1; + this.events = []; + } else { + // Pick up recording where it was left off in another tab or window + this.currentSequenceId = storedSeqId; + this.events = lastSequence.events; } - this.events = []; - const currentSessionStoredEvents = this.config.sessionId && storedReplaySessions[this.config.sessionId]; - this.currentSequenceId = currentSessionStoredEvents ? currentSessionStoredEvents.sequenceId + 1 : 0; - void this.storeEventsForSession([], this.currentSequenceId, this.config.sessionId as number); + } + if (shouldSendStoredEvents && storedReplaySessions) { + this.sendStoredEvents(storedReplaySessions); } if (!this.stopRecordingEvents) { this.recordEvents(); } } + sendStoredEvents(storedReplaySessions: IDBStore) { + for (const sessionId in storedReplaySessions) { + const storedSequences = storedReplaySessions[sessionId].sessionSequences; + for (const storedSeqId in storedSequences) { + const seq = storedSequences[storedSeqId]; + const numericSeqId = parseInt(storedSeqId, 10); + const numericSessionId = parseInt(sessionId, 10); + if (numericSessionId === this.config.sessionId && numericSeqId === this.currentSequenceId) { + continue; + } + if (seq.events.length && seq.status === RecordingStatus.RECORDING) { + this.sendEventsList({ + events: seq.events, + sequenceId: numericSeqId, + sessionId: numericSessionId, + }); + } + } + } + } + recordEvents() { this.stopRecordingEvents = record({ emit: (event) => { - console.log('recording event', event); const eventString = JSON.stringify(event); const shouldSplit = this.shouldSplitEventsList(eventString); @@ -151,7 +147,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { this.events.push(eventString); void this.storeEventsForSession(this.events, this.currentSequenceId, this.config.sessionId as number); }, - // packFn: pack, + packFn: pack, maskAllInputs: true, }); } @@ -169,7 +165,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { return true; } if (this.timeAtLastSend !== null && Date.now() - this.timeAtLastSend > this.interval && this.events.length) { - // this.interval = Math.min(MAX_INTERVAL, this.interval + MIN_INTERVAL); + this.interval = Math.min(MAX_INTERVAL, this.interval + MIN_INTERVAL); this.timeAtLastSend = Date.now(); return true; } @@ -314,12 +310,26 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { async storeEventsForSession(events: Events, sequenceId: number, sessionId: number) { try { - await IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore | undefined): IDBStore => { + await IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore = {}): IDBStore => { + const session = sessionMap[sessionId] || { + currentSequenceId: 0, + sessionSequences: [], + }; + session.currentSequenceId = sequenceId; + + const currentSequence = (session.sessionSequences && session.sessionSequences[sequenceId]) || {}; + + currentSequence.events = events; + currentSequence.status = RecordingStatus.RECORDING; + return { ...sessionMap, [sessionId]: { - events: events, - sequenceId, + ...session, + sessionSequences: { + ...session.sessionSequences, + [sequenceId]: currentSequence, + }, }, }; }); @@ -328,10 +338,25 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } } - async removeSessionEventsStore(sessionId: number) { + async cleanUpSessionEventsStore(sessionId: number, sequenceId: number) { try { await IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore = {}): IDBStore => { - delete sessionMap[sessionId]; + const session = sessionMap[sessionId]; + const sequenceToUpdate = session?.sessionSequences && session.sessionSequences[sequenceId]; + if (!sequenceToUpdate) { + return sessionMap; + } + + sequenceToUpdate.events = []; + sequenceToUpdate.status = RecordingStatus.SENT; + + Object.entries(session.sessionSequences).forEach(([storedSeqId, sequence]) => { + const numericStoredSeqId = parseInt(storedSeqId, 10); + if (sequence.status === RecordingStatus.SENT && sequenceId !== numericStoredSeqId) { + delete session.sessionSequences[numericStoredSeqId]; + } + }); + return sessionMap; }); } catch (e) { @@ -350,7 +375,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { success?: string; removeEvents?: boolean; }) { - removeEvents && context.sessionId && this.storeEventsForSession([], context.sequenceId, context.sessionId); + removeEvents && context.sessionId && this.cleanUpSessionEventsStore(context.sessionId, context.sequenceId); if (err) { this.config.loggerProvider.error(err); } else if (success) { diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index 1fa822259..823999f7d 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -14,10 +14,21 @@ export interface SessionReplayContext { sessionId: number; } +export enum RecordingStatus { + RECORDING = 'recording', + SENDING = 'sending', + SENT = 'sent', +} + +export interface IDBStoreSequence { + events: Events; + status: RecordingStatus; +} + export interface IDBStore { [sessionId: number]: { - events: Events; - sequenceId: number; + currentSequenceId: number; + sessionSequences: { [sequenceId: number]: IDBStoreSequence }; }; } export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { @@ -31,7 +42,7 @@ export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { timeAtLastSend: number | null; stopRecordingEvents: ReturnType | null; maxPersistedEventsSize: number; - emptyStoreAndReset: () => Promise; + initialize: (shouldSendStoredEvents?: boolean) => Promise; recordEvents: () => void; shouldSplitEventsList: (nextEventString: string) => boolean; sendEventsList: ({ @@ -60,7 +71,7 @@ export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { }): void; getAllSessionEventsFromStore: () => Promise; storeEventsForSession: (events: Events, sequenceId: number, sessionId: number) => Promise; - removeSessionEventsStore: (sessionId: number) => Promise; + cleanUpSessionEventsStore: (sessionId: number, sequenceId: number) => Promise; } export interface SessionReplayPlugin { diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 03b044688..01e412277 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -1,10 +1,12 @@ /* eslint-disable jest/expect-expect */ +import * as AnalyticsClientCommon from '@amplitude/analytics-client-common'; import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; import { BrowserConfig, LogLevel } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import * as RRWeb from 'rrweb'; import { SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from '../src/messages'; import { sessionReplayPlugin } from '../src/session-replay'; +import { IDBStore, RecordingStatus } from '../src/typings/session-replay'; jest.mock('idb-keyval'); type MockedIDBKeyVal = jest.Mocked; @@ -42,6 +44,7 @@ describe('SessionReplayPlugin', () => { const { get, update } = IDBKeyVal as MockedIDBKeyVal; const { record } = RRWeb as MockedRRWeb; let originalFetch: typeof global.fetch; + let addEventListenerMock: jest.Mock; const mockConfig: BrowserConfig = { apiKey: 'static_key', flushIntervalMillis: 0, @@ -85,6 +88,12 @@ describe('SessionReplayPlugin', () => { status: 200, }), ) as jest.Mock; + addEventListenerMock = jest.fn() as typeof addEventListenerMock; + jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValueOnce({ + window: { + addEventListener: addEventListenerMock, + } as unknown as Window, + } as Window & typeof globalThis); }); afterEach(() => { jest.clearAllMocks(); @@ -105,31 +114,81 @@ describe('SessionReplayPlugin', () => { expect(sessionReplay.storageKey).toBe('AMP_replay_unsent_static_key'); }); - test('should read events from storage and send them, then reset storage for session', async () => { + test('should call initalize with shouldSendStoredEvents=true', async () => { + const sessionReplay = sessionReplayPlugin(); + const initalize = jest.spyOn(sessionReplay, 'initialize').mockReturnValueOnce(Promise.resolve()); + await sessionReplay.setup(mockConfig); + + expect(initalize).toHaveBeenCalledTimes(1); + + expect(initalize.mock.calls[0]).toEqual([true]); + }); + test('should set up blur and focus event listeners', async () => { + const sessionReplay = sessionReplayPlugin(); + const stopRecordingMock = jest.fn(); + sessionReplay.stopRecordingEvents = stopRecordingMock; + const initialize = jest.spyOn(sessionReplay, 'initialize').mockReturnValueOnce(Promise.resolve()); + await sessionReplay.setup(mockConfig); + initialize.mockReset(); + expect(addEventListenerMock).toHaveBeenCalledTimes(2); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(addEventListenerMock.mock.calls[0][0]).toEqual('blur'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + const blurCallback = addEventListenerMock.mock.calls[0][1]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + blurCallback(); + expect(stopRecordingMock).toHaveBeenCalled(); + expect(sessionReplay.stopRecordingEvents).toEqual(null); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(addEventListenerMock.mock.calls[1][0]).toEqual('focus'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + const focusCallback = addEventListenerMock.mock.calls[1][1]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + focusCallback(); + expect(initialize).toHaveBeenCalled(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(initialize.mock.calls[0]).toEqual([]); + }); + }); + + describe('initalize', () => { + test('should read events from storage and send them if shouldSendStoredEvents is true', async () => { const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, sessionId: 456, }; - const mockGetResolution = Promise.resolve({ + sessionReplay.config = config; + const mockGetResolution: Promise = Promise.resolve({ 123: { - events: [mockEventString], - sequenceId: 3, + currentSequenceId: 3, + sessionSequences: { + 3: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, }, 456: { - events: [mockEventString], - sequenceId: 1, + currentSequenceId: 1, + sessionSequences: { + 1: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, }, }); get.mockReturnValueOnce(mockGetResolution); const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); - await sessionReplay.setup(config); + await sessionReplay.initialize(true); await mockGetResolution; jest.runAllTimers(); - expect(send).toHaveBeenCalledTimes(2); + expect(send).toHaveBeenCalledTimes(1); - // Sending first stored session events + // Should send only events from sequences marked as recording and not current session expect(send.mock.calls[0][0]).toEqual({ attempts: 1, events: [mockEventString], @@ -137,48 +196,98 @@ describe('SessionReplayPlugin', () => { sessionId: 123, timeout: 0, }); - // Sending second stored session events - expect(send.mock.calls[1][0]).toEqual({ - attempts: 1, - events: [mockEventString], - sequenceId: 1, + }); + test('should return early if using old format of IDBStore', async () => { + const sessionReplay = sessionReplayPlugin(); + const config = { + ...mockConfig, sessionId: 456, - timeout: 0, - }); - - expect(update).toHaveBeenCalledTimes(1); - expect(update.mock.calls[0][1]({})).toEqual({ - '456': { - events: [], - sequenceId: 2, + }; + sessionReplay.config = config; + const mockGetResolution = Promise.resolve({ + 123: { + events: [mockEventString], + sequenceId: 1, + }, + 456: { + events: [mockEventString], + sequenceId: 1, }, }); + get.mockReturnValueOnce(mockGetResolution); + const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); + + await sessionReplay.initialize(true); + await mockGetResolution; + jest.runAllTimers(); + expect(send).toHaveBeenCalledTimes(0); }); - test('should handle no stored events', async () => { + test('should return early if session id not set', async () => { const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, + sessionId: undefined, }; - const mockGetResolution = Promise.resolve({}); + sessionReplay.config = config; + const getAllSessionEventsFromStore = jest + .spyOn(sessionReplay, 'getAllSessionEventsFromStore') + .mockReturnValueOnce(Promise.resolve({})); + await sessionReplay.initialize(); + expect(getAllSessionEventsFromStore).not.toHaveBeenCalled(); + }); + test('should configure current sequence id and events correctly if last sequence was sending', async () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + const mockGetResolution: Promise = Promise.resolve({ + 123: { + currentSequenceId: 3, + sessionSequences: { + 3: { + events: [mockEventString], + status: RecordingStatus.SENDING, + }, + }, + }, + }); get.mockReturnValueOnce(mockGetResolution); - await sessionReplay.setup(config); - await mockGetResolution; - expect(sessionReplay.currentSequenceId).toBe(0); - expect(update).toHaveBeenCalledTimes(1); - expect(update.mock.calls[0][1]({})).toEqual({ - '123': { - events: [], - sequenceId: 0, + await sessionReplay.initialize(); + expect(sessionReplay.currentSequenceId).toEqual(4); + expect(sessionReplay.events).toEqual([]); + }); + test('should configure current sequence id and events correctly if last sequence was recording', async () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + const mockGetResolution: Promise = Promise.resolve({ + 123: { + currentSequenceId: 3, + sessionSequences: { + 3: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, }, }); + get.mockReturnValueOnce(mockGetResolution); + await sessionReplay.initialize(); + expect(sessionReplay.currentSequenceId).toEqual(3); + expect(sessionReplay.events).toEqual([mockEventString]); }); - test('should record events', async () => { + test('should handle no stored events', async () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; const mockGetResolution = Promise.resolve({}); get.mockReturnValueOnce(mockGetResolution); + await sessionReplay.initialize(); + expect(sessionReplay.currentSequenceId).toBe(0); + expect(sessionReplay.events).toEqual([]); + }); + test('should record events', async () => { const sessionReplay = sessionReplayPlugin(); - await sessionReplay.setup(mockConfig); - await mockGetResolution; - jest.runAllTimers(); + sessionReplay.config = mockConfig; + const mockGetResolution = Promise.resolve({}); + get.mockReturnValueOnce(mockGetResolution); + await sessionReplay.initialize(); expect(record).toHaveBeenCalledTimes(1); }); }); @@ -246,48 +355,25 @@ describe('SessionReplayPlugin', () => { }); describe('recordEvents', () => { - test('should send the first two emitted events immediately', () => { - const sessionReplay = sessionReplayPlugin(); - const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); - sessionReplay.config = mockConfig; - sessionReplay.recordEvents(); - // Confirm that no events have been set in events list yet - expect(sessionReplay.events).toEqual([]); - const recordArg = record.mock.calls[0][0]; - // Emit two events manually (replicating what rrweb would do itself) - recordArg?.emit && recordArg?.emit(mockEvent); - recordArg?.emit && recordArg?.emit(mockEvent); - // Confirm that there are still no events in the events list - // (because they are sent immediately instead of stored) - expect(sessionReplay.events).toEqual([]); - jest.runAllTimers(); - expect(send).toHaveBeenCalledTimes(1); - expect(send.mock.calls[0][0]).toEqual({ - events: [mockEventString, mockEventString], - sequenceId: 0, - attempts: 1, - timeout: 0, - sessionId: 123, - }); - }); - test('should store events in class and in IDB', () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; sessionReplay.recordEvents(); expect(sessionReplay.events).toEqual([]); const recordArg = record.mock.calls[0][0]; - // Emit first two events, which get sent immediately - recordArg?.emit && recordArg?.emit(mockEvent); - recordArg?.emit && recordArg?.emit(mockEvent); - // Emit third event, which is stored in class and IDB + // Emit event, which is stored in class and IDB recordArg?.emit && recordArg?.emit(mockEvent); expect(sessionReplay.events).toEqual([mockEventString]); - expect(update).toHaveBeenCalledTimes(2); - expect(update.mock.calls[1][1]({})).toEqual({ + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][1]({})).toEqual({ 123: { - events: [mockEventString], - sequenceId: 1, + currentSequenceId: 0, + sessionSequences: { + 0: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, }, }); }); @@ -300,24 +386,20 @@ describe('SessionReplayPlugin', () => { const dateNowMock = jest.spyOn(Date, 'now').mockReturnValue(1); const sendEventsList = jest.spyOn(sessionReplay, 'sendEventsList'); const recordArg = record.mock.calls[0][0]; - // Emit first two events, which get sent immediately - recordArg?.emit && recordArg?.emit(mockEvent); - recordArg?.emit && recordArg?.emit(mockEvent); - expect(sendEventsList).toHaveBeenCalledTimes(1); - sendEventsList.mockClear(); - // Emit third event, which is not sent immediately + // Emit first event, which is not sent immediately recordArg?.emit && recordArg?.emit(mockEvent); expect(sendEventsList).toHaveBeenCalledTimes(0); - // Emit fourth event and advance timers to interval + // Emit second event and advance timers to interval dateNowMock.mockReturnValue(1002); recordArg?.emit && recordArg?.emit(mockEvent); expect(sendEventsList).toHaveBeenCalledTimes(1); expect(sendEventsList).toHaveBeenCalledWith({ events: [mockEventString], - sequenceId: 1, + sequenceId: 0, sessionId: 123, }); expect(sessionReplay.events).toEqual([mockEventString]); + expect(sessionReplay.currentSequenceId).toEqual(1); dateNowMock.mockClear(); }); @@ -325,7 +407,7 @@ describe('SessionReplayPlugin', () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; sessionReplay.maxPersistedEventsSize = 20; - sessionReplay.currentSequenceId = 1; + // Simulate as if many events have already been built up const events = ['#'.repeat(20)]; sessionReplay.events = events; // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -338,7 +420,7 @@ describe('SessionReplayPlugin', () => { expect(sendEventsListMock).toHaveBeenCalledTimes(1); expect(sendEventsListMock).toHaveBeenCalledWith({ events, - sequenceId: 1, + sequenceId: 0, sessionId: 123, }); @@ -346,8 +428,13 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1]({})).toEqual({ 123: { - events: [mockEventString], - sequenceId: 2, + currentSequenceId: 1, + sessionSequences: { + 1: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, }, }); }); @@ -510,7 +597,7 @@ describe('SessionReplayPlugin', () => { method: 'POST', }); }); - test('should remove session events from IDB store upon success', async () => { + test('should update IDB store upon success', async () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; const context = { @@ -520,26 +607,13 @@ describe('SessionReplayPlugin', () => { attempts: 0, timeout: 0, }; + const cleanUpSessionEventsStore = jest + .spyOn(sessionReplay, 'cleanUpSessionEventsStore') + .mockReturnValueOnce(Promise.resolve()); await sessionReplay.send(context); jest.runAllTimers(); - const mockIDBStore = { - 123: { - events: [mockEventString], - sequenceId: 3, - }, - 456: { - events: [mockEventString], - sequenceId: 1, - }, - }; - - expect(update).toHaveBeenCalledTimes(1); - expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ - 456: { - events: [mockEventString], - sequenceId: 1, - }, - }); + expect(cleanUpSessionEventsStore).toHaveBeenCalledTimes(1); + expect(cleanUpSessionEventsStore.mock.calls[0]).toEqual([123, 1]); }); test('should not remove session events from IDB store upon failure', async () => { const sessionReplay = sessionReplayPlugin(); @@ -768,8 +842,8 @@ describe('SessionReplayPlugin', () => { }); }); - describe('idb error handling', () => { - test('getAllSessionEventsFromStore should catch errors', async () => { + describe('getAllSessionEventsFromStore', () => { + test('should catch errors', async () => { const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, @@ -784,7 +858,117 @@ describe('SessionReplayPlugin', () => { 'Failed to store session replay events in IndexedDB: error', ); }); - test('storeEventsForSession should catch errors', async () => { + }); + + describe('cleanUpSessionEventsStore', () => { + test('should update events and status for current session', async () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + const mockIDBStore: IDBStore = { + 123: { + currentSequenceId: 3, + sessionSequences: { + 2: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + 3: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, + }, + 456: { + currentSequenceId: 1, + sessionSequences: { + 1: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + }; + await sessionReplay.cleanUpSessionEventsStore(123, 3); + + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ + 123: { + currentSequenceId: 3, + sessionSequences: { + 2: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + 3: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + 456: { + currentSequenceId: 1, + sessionSequences: { + 1: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + }); + }); + + test('should delete sent sequences for current session', async () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + const mockIDBStore: IDBStore = { + 123: { + currentSequenceId: 3, + sessionSequences: { + 2: { + events: [], + status: RecordingStatus.SENT, + }, + 3: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, + }, + 456: { + currentSequenceId: 1, + sessionSequences: { + 1: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + }; + await sessionReplay.cleanUpSessionEventsStore(123, 3); + + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ + 123: { + currentSequenceId: 3, + sessionSequences: { + 3: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + 456: { + currentSequenceId: 1, + sessionSequences: { + 1: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + }); + }); + test('should catch errors', async () => { const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, @@ -792,14 +976,164 @@ describe('SessionReplayPlugin', () => { }; sessionReplay.config = config; update.mockImplementationOnce(() => Promise.reject('error')); - await sessionReplay.storeEventsForSession([mockEventString], 0); + await sessionReplay.cleanUpSessionEventsStore(123, 1); expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( 'Failed to store session replay events in IndexedDB: error', ); }); - test('removeSessionEventsStore should catch errors', async () => { + test('should handle an undefined store', async () => { + const sessionReplay = sessionReplayPlugin(); + const config = { + ...mockConfig, + loggerProvider: mockLoggerProvider, + }; + sessionReplay.config = config; + update.mockImplementationOnce(() => Promise.resolve()); + await sessionReplay.cleanUpSessionEventsStore(123, 1); + expect(update.mock.calls[0][1](undefined)).toEqual({}); + }); + }); + + describe('storeEventsForSession', () => { + test('should update the session current sequence id, and the current sequence events and status', async () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + const mockIDBStore: IDBStore = { + 123: { + currentSequenceId: 2, + sessionSequences: { + 2: { + events: [], + status: RecordingStatus.RECORDING, + }, + }, + }, + 456: { + currentSequenceId: 1, + sessionSequences: { + 1: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + }; + await sessionReplay.storeEventsForSession([mockEventString], 2, 123); + + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ + 123: { + currentSequenceId: 2, + sessionSequences: { + 2: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, + }, + 456: { + currentSequenceId: 1, + sessionSequences: { + 1: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + }); + }); + test('should add a new entry if none exist for sequence id', async () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + const mockIDBStore: IDBStore = { + 123: { + currentSequenceId: 1, + sessionSequences: { + 1: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + 456: { + currentSequenceId: 1, + sessionSequences: { + 1: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + }; + await sessionReplay.storeEventsForSession([mockEventString], 2, 123); + + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ + 123: { + currentSequenceId: 2, + sessionSequences: { + 1: { + events: [], + status: RecordingStatus.SENT, + }, + 2: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, + }, + 456: { + currentSequenceId: 1, + sessionSequences: { + 1: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + }); + }); + test('should add a new entry if none exist for session id', async () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + const mockIDBStore: IDBStore = { + 123: { + currentSequenceId: 2, + sessionSequences: { + 2: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + }; + await sessionReplay.storeEventsForSession([mockEventString], 0, 456); + + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ + 123: { + currentSequenceId: 2, + sessionSequences: { + 2: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + 456: { + currentSequenceId: 0, + sessionSequences: { + 0: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, + }, + }); + }); + test('should catch errors', async () => { const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, @@ -807,14 +1141,14 @@ describe('SessionReplayPlugin', () => { }; sessionReplay.config = config; update.mockImplementationOnce(() => Promise.reject('error')); - await sessionReplay.removeSessionEventsStore(123); + await sessionReplay.storeEventsForSession([mockEventString], 0, config.sessionId as number); expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( 'Failed to store session replay events in IndexedDB: error', ); }); - test('removeSessionEventsStore should handle an undefined store', async () => { + test('should handle an undefined store', async () => { const sessionReplay = sessionReplayPlugin(); const config = { ...mockConfig, @@ -822,8 +1156,18 @@ describe('SessionReplayPlugin', () => { }; sessionReplay.config = config; update.mockImplementationOnce(() => Promise.resolve()); - await sessionReplay.removeSessionEventsStore(123); - expect(update.mock.calls[0][1](undefined)).toEqual({}); + await sessionReplay.storeEventsForSession([mockEventString], 0, 456); + expect(update.mock.calls[0][1](undefined)).toEqual({ + 456: { + currentSequenceId: 0, + sessionSequences: { + 0: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, + }, + }); }); }); @@ -863,27 +1207,15 @@ describe('SessionReplayPlugin', () => { const result = sessionReplay.shouldSplitEventsList(nextEvent); expect(result).toBe(false); }); - test('should return true if it has been long enough since last send', () => { + test('should return true if it has been long enough since last send and events have been emitted', () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.timeAtLastSend = 1; - jest.spyOn(Date, 'now').mockReturnValue(1002); + sessionReplay.events = [mockEventString]; + jest.spyOn(Date, 'now').mockReturnValue(100002); const nextEvent = 'a'; const result = sessionReplay.shouldSplitEventsList(nextEvent); expect(result).toBe(true); }); - test('should increase interval incrementally', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.timeAtLastSend = 1; - jest.spyOn(Date, 'now').mockReturnValue(1000000); - const nextEvent = 'a'; - for (let i = 1; i <= 10; i++) { - expect(sessionReplay.interval).toEqual(1000 * i); - sessionReplay.shouldSplitEventsList(nextEvent); - } - // Call one more time to go over the 10 max - sessionReplay.shouldSplitEventsList(nextEvent); - expect(sessionReplay.interval).toEqual(10000); - }); }); }); }); From b770d44086306db15a378af6b1c3590afdb0ec58 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 20 Jul 2023 13:51:59 -0400 Subject: [PATCH 060/214] fix(plugins): reduce min interval to 500ms --- packages/plugin-session-replay-browser/src/session-replay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index ffcc7e669..719a70a9b 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -18,7 +18,7 @@ const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/tra const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; -const MIN_INTERVAL = 1 * 1000; // 1 second +const MIN_INTERVAL = 500; // 500 ms const MAX_INTERVAL = 10 * 1000; // 10 seconds class SessionReplay implements SessionReplayEnrichmentPlugin { From 2c95641a226169afa89936f06afbbeef030f78e1 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 20 Jul 2023 18:10:45 +0000 Subject: [PATCH 061/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.2.0 --- .../plugin-session-replay-browser/CHANGELOG.md | 17 +++++++++++++++++ .../plugin-session-replay-browser/package.json | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index dacc6d34f..435418cf4 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.2.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.1.3...@amplitude/plugin-session-replay-browser@0.2.0) (2023-07-20) + +### Bug Fixes + +- **plugins:** reduce min interval to 500ms + ([b770d44](https://github.com/amplitude/Amplitude-TypeScript/commit/b770d44086306db15a378af6b1c3590afdb0ec58)) + +### Features + +- **plugins:** solve timing issues with multiple tabs + ([d2e9a0c](https://github.com/amplitude/Amplitude-TypeScript/commit/d2e9a0cc7c455dcdf13ea2fa303d2fbd50911536)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.1.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.1.2...@amplitude/plugin-session-replay-browser@0.1.3) (2023-07-13) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index d7e7203c0..27fcc3b5f 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.1.3", + "version": "0.2.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From d85e230193b0c4286586fb23a8e020413051c595 Mon Sep 17 00:00:00 2001 From: Alyssa Yu Date: Tue, 25 Jul 2023 15:58:34 -0700 Subject: [PATCH 062/214] feat: user agent plugin (#503) --- .../README.md | 75 +++++++++++ .../jest.config.js | 10 ++ .../package.json | 57 +++++++++ .../rollup.config.js | 6 + .../src/index.ts | 2 + .../src/typings/ua-parser.d.ts | 4 + .../typings/user-agent-enrichment-plugin.ts | 13 ++ .../src/user-agent-enrichment-plugin.ts | 43 +++++++ .../test/user-agent.test.ts | 120 ++++++++++++++++++ .../tsconfig.es5.json | 10 ++ .../tsconfig.esm.json | 10 ++ .../tsconfig.json | 11 ++ 12 files changed, 361 insertions(+) create mode 100644 packages/plugin-user-agent-enrichment-browser/README.md create mode 100644 packages/plugin-user-agent-enrichment-browser/jest.config.js create mode 100644 packages/plugin-user-agent-enrichment-browser/package.json create mode 100644 packages/plugin-user-agent-enrichment-browser/rollup.config.js create mode 100644 packages/plugin-user-agent-enrichment-browser/src/index.ts create mode 100644 packages/plugin-user-agent-enrichment-browser/src/typings/ua-parser.d.ts create mode 100644 packages/plugin-user-agent-enrichment-browser/src/typings/user-agent-enrichment-plugin.ts create mode 100644 packages/plugin-user-agent-enrichment-browser/src/user-agent-enrichment-plugin.ts create mode 100644 packages/plugin-user-agent-enrichment-browser/test/user-agent.test.ts create mode 100644 packages/plugin-user-agent-enrichment-browser/tsconfig.es5.json create mode 100644 packages/plugin-user-agent-enrichment-browser/tsconfig.esm.json create mode 100644 packages/plugin-user-agent-enrichment-browser/tsconfig.json diff --git a/packages/plugin-user-agent-enrichment-browser/README.md b/packages/plugin-user-agent-enrichment-browser/README.md new file mode 100644 index 000000000..dca10b9c8 --- /dev/null +++ b/packages/plugin-user-agent-enrichment-browser/README.md @@ -0,0 +1,75 @@ +

+ + + +
+

+ +# @amplitude/plugin-user-agent-enrichment-browser + +Official Browser SDK plugin for user agent enrichment. + +## Installation + +This package is published on NPM registry and is available to be installed using npm and yarn. + +```sh +# npm +npm install @amplitude/plugin-user-agent-enrichment-browser@^1.0.0 + +# yarn +yarn add @amplitude/plugin-user-agent-enrichment-browser@^1.0.0 +``` + +## Usage + +This plugin works on top of the Amplitude Browser SDK. It's used for enriching events with user agent information using the @amplitude/ua-parser-js. The user agent identifies the application, operating system, vendor, and/or version of the requesting client. +For Browser SDK v1.x, we use @amplitude/ua-parser-js internally to parse the user agent information. In Browser v2.x, we have removed this client-side user agent parser and have instead implemented server-side user agent parser. You can use this plugin to maintain consistency with the user agent information from earlier SDK versions. + +### 1. Import Amplitude packages + +* `@amplitude/analytics-browser` +* `@amplitude/plugin-user-agent-enrichment-browser` + +```typescript +import * as amplitude from '@amplitude/analytics-browser'; +import { userAgentEnrichmentPlugin } from '@amplitude/plugin-user-agent-enrichment-browser'; +``` + +### 2. Instantiate user agent enrichment plugin + +The plugin accepts 1 optional parameter, which is an `Object` to disable/enable the corresponding tracking options. Each option is enabled by default. + +```typescript +const uaPlugin = userAgentEnrichmentPlugin({ + osName: true, + osVersion: true, + deviceManufacturer: false, + deviceModel: false, +}); +``` + +#### Options + +|Name|Type|Default|Description| +|-|-|-|-| +|`osName`|`boolean`|`true`| Enables enrichment of `os_name` property. | +|`osVersion`|`boolean`|`true`| Enables enrichment of `os_version` property. | +|`deviceManufacturer`|`boolean`|`true`| Enables enrichment of `device_manufacturer` property. | +|`deviceModel`|`boolean`|`true`| Enables enrichment of `device_model` property. | + +### 3. Install plugin to Amplitude SDK + +```typescript +amplitude.add(uaPlugin); +``` + +### 4. Initialize Amplitude SDK + +```typescript +amplitude.init('API_KEY'); +``` + +## Resulting page view event + +This plugin parses user agent information using @amplitude/ua-parser-js and enriches events based on your configuration. This affects the value of the following properties: `device_family`, `device_model`, `device_manufacturer`, `device_type`, `os`, `os_name`, and `os_version`. \ No newline at end of file diff --git a/packages/plugin-user-agent-enrichment-browser/jest.config.js b/packages/plugin-user-agent-enrichment-browser/jest.config.js new file mode 100644 index 000000000..a0ef09957 --- /dev/null +++ b/packages/plugin-user-agent-enrichment-browser/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../jest.config.js'); +const package = require('./package'); + +module.exports = { + ...baseConfig, + displayName: package.name, + rootDir: '.', + testEnvironment: 'jsdom', + coveragePathIgnorePatterns: ['index.ts'], +}; \ No newline at end of file diff --git a/packages/plugin-user-agent-enrichment-browser/package.json b/packages/plugin-user-agent-enrichment-browser/package.json new file mode 100644 index 000000000..73bffb4e0 --- /dev/null +++ b/packages/plugin-user-agent-enrichment-browser/package.json @@ -0,0 +1,57 @@ +{ + "name": "@amplitude/plugin-user-agent-enrichment-browser", + "version": "0.0.0", + "description": "", + "author": "Amplitude Inc", + "homepage": "https://github.com/amplitude/Amplitude-TypeScript", + "license": "MIT", + "main": "lib/cjs/index.js", + "module": "lib/esm/index.js", + "types": "lib/esm/index.d.ts", + "sideEffects": false, + "publishConfig": { + "access": "public", + "tag": "latest" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/amplitude/Amplitude-TypeScript.git" + }, + "scripts": { + "build": "yarn bundle && yarn build:es5 && yarn build:esm", + "bundle": "rollup --config rollup.config.js", + "build:es5": "tsc -p ./tsconfig.es5.json", + "build:esm": "tsc -p ./tsconfig.esm.json", + "clean": "rimraf node_modules lib coverage", + "fix": "yarn fix:eslint & yarn fix:prettier", + "fix:eslint": "eslint '{src,test}/**/*.ts' --fix", + "fix:prettier": "prettier --write \"{src,test}/**/*.ts\"", + "lint": "yarn lint:eslint & yarn lint:prettier", + "lint:eslint": "eslint '{src,test}/**/*.ts'", + "lint:prettier": "prettier --check \"{src,test}/**/*.ts\"", + "test": "jest", + "typecheck": "tsc -p ./tsconfig.json" + }, + "bugs": { + "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" + }, + "dependencies": { + "@amplitude/analytics-client-common": "^2.0.4", + "@amplitude/analytics-types": "^2.1.1", + "@amplitude/ua-parser-js": "^0.7.33", + "tslib": "^2.4.1" + }, + "devDependencies": { + "@amplitude/analytics-browser": "^2.1.2", + "@rollup/plugin-commonjs": "^23.0.4", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^10.0.1", + "rollup": "^2.79.1", + "rollup-plugin-execute": "^1.1.1", + "rollup-plugin-gzip": "^3.1.0", + "rollup-plugin-terser": "^7.0.2" + }, + "files": [ + "lib" + ] +} diff --git a/packages/plugin-user-agent-enrichment-browser/rollup.config.js b/packages/plugin-user-agent-enrichment-browser/rollup.config.js new file mode 100644 index 000000000..8b1d5414a --- /dev/null +++ b/packages/plugin-user-agent-enrichment-browser/rollup.config.js @@ -0,0 +1,6 @@ +import { iife, umd } from '../../scripts/build/rollup.config'; + +iife.input = umd.input; +iife.output.name = 'amplitudeUserAgentEnrichmentPlugin'; + +export default [umd, iife]; diff --git a/packages/plugin-user-agent-enrichment-browser/src/index.ts b/packages/plugin-user-agent-enrichment-browser/src/index.ts new file mode 100644 index 000000000..594daf69f --- /dev/null +++ b/packages/plugin-user-agent-enrichment-browser/src/index.ts @@ -0,0 +1,2 @@ +export { userAgentEnrichmentPlugin } from './user-agent-enrichment-plugin'; +export { userAgentEnrichmentPlugin as plugin } from './user-agent-enrichment-plugin'; diff --git a/packages/plugin-user-agent-enrichment-browser/src/typings/ua-parser.d.ts b/packages/plugin-user-agent-enrichment-browser/src/typings/ua-parser.d.ts new file mode 100644 index 000000000..bac2f15e9 --- /dev/null +++ b/packages/plugin-user-agent-enrichment-browser/src/typings/ua-parser.d.ts @@ -0,0 +1,4 @@ +declare module '@amplitude/ua-parser-js' { + import UAParser from 'ua-parser-js'; + export = UAParser; +} diff --git a/packages/plugin-user-agent-enrichment-browser/src/typings/user-agent-enrichment-plugin.ts b/packages/plugin-user-agent-enrichment-browser/src/typings/user-agent-enrichment-plugin.ts new file mode 100644 index 000000000..0385293c3 --- /dev/null +++ b/packages/plugin-user-agent-enrichment-browser/src/typings/user-agent-enrichment-plugin.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/no-empty-interface */ +import { EnrichmentPlugin } from '@amplitude/analytics-types'; + +export interface Options { + osName?: boolean; + osVersion?: boolean; + deviceManufacturer?: boolean; + deviceModel?: boolean; +} + +export interface CreateUserAgentEnrichmentPlugin { + (options?: Options): EnrichmentPlugin; +} diff --git a/packages/plugin-user-agent-enrichment-browser/src/user-agent-enrichment-plugin.ts b/packages/plugin-user-agent-enrichment-browser/src/user-agent-enrichment-plugin.ts new file mode 100644 index 000000000..de7fd34aa --- /dev/null +++ b/packages/plugin-user-agent-enrichment-browser/src/user-agent-enrichment-plugin.ts @@ -0,0 +1,43 @@ +import { BrowserConfig, EnrichmentPlugin, Event } from '@amplitude/analytics-types'; +import { CreateUserAgentEnrichmentPlugin, Options } from './typings/user-agent-enrichment-plugin'; +import UAParser from '@amplitude/ua-parser-js'; + +export const userAgentEnrichmentPlugin: CreateUserAgentEnrichmentPlugin = function (options: Options = {}) { + const { osName = true, osVersion = true, deviceManufacturer = true, deviceModel = true } = options; + + let uaResult: UAParser.IResult; + + const plugin: EnrichmentPlugin = { + name: '@amplitude/plugin-user-agent-enrichment-browser', + type: 'enrichment', + + setup: async function (config: BrowserConfig) { + let userAgent: string | undefined; + /* istanbul ignore else */ + if (typeof navigator !== 'undefined') { + userAgent = navigator.userAgent; + } + + uaResult = new UAParser(userAgent).getResult(); + + config.loggerProvider.log('Installing @amplitude/plugin-user-agent-enrichment-browser.'); + }, + + execute: async (event: Event) => { + const uaOsName = uaResult.browser.name; + const UaOsVersion = uaResult.browser.version; + const UaDeviceModel = uaResult.device.model || uaResult.os.name; + const UaDeviceVendor = uaResult.device.vendor; + + return { + ...event, + ...(osName && { os_name: uaOsName }), + ...(osVersion && { os_version: UaOsVersion }), + ...(deviceManufacturer && { device_manufacturer: UaDeviceVendor }), + ...(deviceModel && { device_model: UaDeviceModel }), + }; + }, + }; + + return plugin; +}; diff --git a/packages/plugin-user-agent-enrichment-browser/test/user-agent.test.ts b/packages/plugin-user-agent-enrichment-browser/test/user-agent.test.ts new file mode 100644 index 000000000..814bcfc9a --- /dev/null +++ b/packages/plugin-user-agent-enrichment-browser/test/user-agent.test.ts @@ -0,0 +1,120 @@ +import { createInstance } from '@amplitude/analytics-browser'; +import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; +import { userAgentEnrichmentPlugin } from '../src/user-agent-enrichment-plugin'; +import { BaseEvent, BrowserConfig, LogLevel } from '@amplitude/analytics-types'; +import { Logger, UUID } from '@amplitude/analytics-core'; + +describe('uaParserPlugin', () => { + let event: BaseEvent; + + const mockConfig: BrowserConfig = { + apiKey: UUID(), + flushIntervalMillis: 0, + flushMaxRetries: 0, + flushQueueSize: 0, + logLevel: LogLevel.None, + loggerProvider: new Logger(), + optOut: false, + serverUrl: undefined, + transportProvider: new FetchTransport(), + useBatch: false, + cookieOptions: { + domain: '.amplitude.com', + expiration: 365, + sameSite: 'Lax', + secure: false, + upgrade: true, + }, + cookieStorage: new CookieStorage(), + sessionTimeout: 30 * 60 * 1000, + trackingOptions: { + ipAddress: true, + language: true, + platform: true, + }, + }; + + beforeEach(() => { + event = { + event_type: 'event_type', + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('execute', () => { + test('should overwirte all devices info without option', async () => { + const amplitude = createInstance(); + + const plugin = userAgentEnrichmentPlugin(); + const executeSpy = jest.spyOn(plugin, 'execute'); + + await plugin.setup?.(mockConfig, amplitude); + const enrichedEvent = await plugin.execute?.(event); + + expect(executeSpy).toHaveBeenCalledWith(event); + expect(enrichedEvent).toHaveProperty('os_name'); + expect(enrichedEvent).toHaveProperty('os_version'); + expect(enrichedEvent).toHaveProperty('os_name'); + expect(enrichedEvent).toHaveProperty('device_model'); + }); + + test('should escape the optOut devices info with disabled options 1', async () => { + const amplitude = createInstance(); + + const plugin = userAgentEnrichmentPlugin({ + osVersion: false, + }); + + await plugin.setup?.(mockConfig, amplitude); + + const enrichedEvent = await plugin.execute?.(event); + + expect(enrichedEvent).toHaveProperty('device_model'); + expect(enrichedEvent).toHaveProperty('os_name'); + expect(enrichedEvent).not.toHaveProperty('os_version'); + expect(enrichedEvent).toHaveProperty('device_manufacturer'); + }); + + test('should escape the optOut devices info with disabled options 2', async () => { + const amplitude = createInstance(); + + const plugin = userAgentEnrichmentPlugin({ + osName: false, + deviceManufacturer: true, + }); + + await plugin.setup?.(mockConfig, amplitude); + + const enrichedEvent = await plugin.execute?.(event); + + expect(enrichedEvent).toHaveProperty('device_model'); + expect(enrichedEvent).not.toHaveProperty('os_name'); + expect(enrichedEvent).toHaveProperty('os_version'); + expect(enrichedEvent).toHaveProperty('device_manufacturer'); + }); + + test('should overwrite the opted in devices info', async () => { + const amplitude = createInstance(); + + const plugin = userAgentEnrichmentPlugin({ + osName: true, + osVersion: true, + deviceManufacturer: false, + deviceModel: false, + }); + + await plugin.setup?.(mockConfig, amplitude); + + const enrichedEvent = await plugin.execute?.(event); + + expect(enrichedEvent).toHaveProperty('os_name'); + expect(enrichedEvent).toHaveProperty('os_version'); + + expect(enrichedEvent).not.toHaveProperty('device_manufacturer'); + expect(enrichedEvent).not.toHaveProperty('device_model'); + }); + }); +}); diff --git a/packages/plugin-user-agent-enrichment-browser/tsconfig.es5.json b/packages/plugin-user-agent-enrichment-browser/tsconfig.es5.json new file mode 100644 index 000000000..77e041d3f --- /dev/null +++ b/packages/plugin-user-agent-enrichment-browser/tsconfig.es5.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"], + "compilerOptions": { + "module": "commonjs", + "noEmit": false, + "outDir": "lib/cjs", + "rootDir": "./src" + } +} diff --git a/packages/plugin-user-agent-enrichment-browser/tsconfig.esm.json b/packages/plugin-user-agent-enrichment-browser/tsconfig.esm.json new file mode 100644 index 000000000..bec981eee --- /dev/null +++ b/packages/plugin-user-agent-enrichment-browser/tsconfig.esm.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"], + "compilerOptions": { + "module": "es6", + "noEmit": false, + "outDir": "lib/esm", + "rootDir": "./src" + } +} diff --git a/packages/plugin-user-agent-enrichment-browser/tsconfig.json b/packages/plugin-user-agent-enrichment-browser/tsconfig.json new file mode 100644 index 000000000..bd2a4cdb7 --- /dev/null +++ b/packages/plugin-user-agent-enrichment-browser/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*", "test/**/*"], + "compilerOptions": { + "baseUrl": ".", + "esModuleInterop": true, + "lib": ["dom"], + "noEmit": true, + "rootDir": ".", + } +} \ No newline at end of file From 7fb6059716a09fae3c62a238d100fecc06861e67 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Wed, 26 Jul 2023 11:01:31 -0700 Subject: [PATCH 063/214] fix(plugins): stop recording when document.hasFocus is false --- .../src/session-replay.ts | 9 +++++-- .../test/session-replay.test.ts | 27 +++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 719a70a9b..8840f4352 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -16,8 +16,8 @@ import { const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/track'; const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; -const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 200; // derived by JSON stringifying an example payload without events -const MAX_EVENT_LIST_SIZE_IN_BYTES = 20 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; +const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 500; // derived by JSON stringifying an example payload without events +const MAX_EVENT_LIST_SIZE_IN_BYTES = 10 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; const MIN_INTERVAL = 500; // 500 ms const MAX_INTERVAL = 10 * 1000; // 10 seconds @@ -132,6 +132,11 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { recordEvents() { this.stopRecordingEvents = record({ emit: (event) => { + const GlobalScope = getGlobalScope(); + if (GlobalScope && GlobalScope.document && !GlobalScope.document.hasFocus()) { + this.stopRecordingEvents && this.stopRecordingEvents(); + return; + } const eventString = JSON.stringify(event); const shouldSplit = this.shouldSplitEventsList(eventString); diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 01e412277..98eb79cbe 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -89,14 +89,17 @@ describe('SessionReplayPlugin', () => { }), ) as jest.Mock; addEventListenerMock = jest.fn() as typeof addEventListenerMock; - jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValueOnce({ + jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue({ window: { addEventListener: addEventListenerMock, } as unknown as Window, + document: { + hasFocus: () => true, + }, } as Window & typeof globalThis); }); afterEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); global.fetch = originalFetch; }); afterAll(() => { @@ -438,6 +441,26 @@ describe('SessionReplayPlugin', () => { }, }); }); + + test('should return early if document is not in focus', () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + sessionReplay.recordEvents(); + sessionReplay.stopRecordingEvents = jest.fn(); + expect(sessionReplay.events).toEqual([]); + jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue({ + document: { + hasFocus: () => false, + }, + } as typeof globalThis); + // eslint-disable-next-line @typescript-eslint/no-empty-function + const sendEventsListMock = jest.spyOn(sessionReplay, 'sendEventsList').mockImplementationOnce(() => {}); + const recordArg = record.mock.calls[0][0]; + recordArg?.emit && recordArg?.emit(mockEvent); + expect(sendEventsListMock).toHaveBeenCalledTimes(0); + expect(sessionReplay.stopRecordingEvents).toHaveBeenCalled(); + expect(sessionReplay.events).toEqual([]); + }); }); describe('addToQueue', () => { From cf93eaf295645232e839e4625cfbbeaa933bfefc Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Wed, 26 Jul 2023 13:44:01 -0700 Subject: [PATCH 064/214] fix: react-native fix context plugin to include app version on mobile (#507) --- .../src/plugins/context.ts | 2 +- .../test/plugins/context.test.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/analytics-react-native/src/plugins/context.ts b/packages/analytics-react-native/src/plugins/context.ts index 8c4414a44..20486ddff 100644 --- a/packages/analytics-react-native/src/plugins/context.ts +++ b/packages/analytics-react-native/src/plugins/context.ts @@ -81,7 +81,7 @@ export class Context implements BeforePlugin { device_id: this.config.deviceId, session_id: this.config.sessionId, time, - ...(this.config.appVersion && { app_version: appVersion }), + ...(appVersion && { app_version: appVersion }), ...(this.config.trackingOptions.platform && { platform: platform }), ...(this.config.trackingOptions.osName && { os_name: osName }), ...(this.config.trackingOptions.osVersion && { os_version: osVersion }), diff --git a/packages/analytics-react-native/test/plugins/context.test.ts b/packages/analytics-react-native/test/plugins/context.test.ts index 65e6ae06b..6cea0fdf5 100644 --- a/packages/analytics-react-native/test/plugins/context.test.ts +++ b/packages/analytics-react-native/test/plugins/context.test.ts @@ -131,6 +131,23 @@ describe('context', () => { expect(secondContextEvent.insert_id).not.toEqual(firstContextEvent.insert_id); }); + test('should contain app version from native module', async () => { + const context = new Context(); + const config = useDefaultConfig({ + deviceId: 'deviceId', + sessionId: 1, + userId: 'user@amplitude.com', + }); + await context.setup(config); + + const event = { + event_type: 'event_type', + }; + const firstContextEvent = await context.execute(event); + + expect(firstContextEvent.app_version).toEqual(isWeb() ? undefined : '1.0.0'); + }); + describe('ingestionMetadata config', () => { test('should include ingestion metadata', async () => { const sourceName = 'ampli'; From 6d9cf5f1a05293815917f2136914493a67a9256f Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Wed, 26 Jul 2023 20:51:04 +0000 Subject: [PATCH 065/214] chore(release): publish - @amplitude/analytics-react-native@1.3.2 - @amplitude/plugin-session-replay-browser@0.2.1 - @amplitude/plugin-user-agent-enrichment-browser@0.1.0 --- packages/analytics-react-native/CHANGELOG.md | 13 +++++++++++++ packages/analytics-react-native/package.json | 2 +- packages/analytics-react-native/src/version.ts | 2 +- packages/plugin-session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/plugin-session-replay-browser/package.json | 2 +- .../CHANGELOG.md | 11 +++++++++++ .../package.json | 2 +- 7 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 packages/plugin-user-agent-enrichment-browser/CHANGELOG.md diff --git a/packages/analytics-react-native/CHANGELOG.md b/packages/analytics-react-native/CHANGELOG.md index d373867a1..5159e4202 100644 --- a/packages/analytics-react-native/CHANGELOG.md +++ b/packages/analytics-react-native/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.3.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-react-native@1.3.1...@amplitude/analytics-react-native@1.3.2) (2023-07-26) + +### Bug Fixes + +- react-native fix context plugin to include app version on mobile + ([#507](https://github.com/amplitude/Amplitude-TypeScript/issues/507)) + ([cf93eaf](https://github.com/amplitude/Amplitude-TypeScript/commit/cf93eaf295645232e839e4625cfbbeaa933bfefc)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.3.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-react-native@1.3.0...@amplitude/analytics-react-native@1.3.1) (2023-07-05) **Note:** Version bump only for package @amplitude/analytics-react-native diff --git a/packages/analytics-react-native/package.json b/packages/analytics-react-native/package.json index c2b8cf914..b8d85189a 100644 --- a/packages/analytics-react-native/package.json +++ b/packages/analytics-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-react-native", - "version": "1.3.1", + "version": "1.3.2", "description": "Official React Native SDK", "keywords": [ "analytics", diff --git a/packages/analytics-react-native/src/version.ts b/packages/analytics-react-native/src/version.ts index 1ef6744a1..45f295c35 100644 --- a/packages/analytics-react-native/src/version.ts +++ b/packages/analytics-react-native/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.3.1'; +export const VERSION = '1.3.2'; diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 435418cf4..39e26d536 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.2.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.2.0...@amplitude/plugin-session-replay-browser@0.2.1) (2023-07-26) + +### Bug Fixes + +- **plugins:** stop recording when document.hasFocus is false + ([7fb6059](https://github.com/amplitude/Amplitude-TypeScript/commit/7fb6059716a09fae3c62a238d100fecc06861e67)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [0.2.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.1.3...@amplitude/plugin-session-replay-browser@0.2.0) (2023-07-20) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 27fcc3b5f..f2a16b50c 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.2.0", + "version": "0.2.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", diff --git a/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md b/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md new file mode 100644 index 000000000..649bb0b8f --- /dev/null +++ b/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.1.0 (2023-07-26) + +### Features + +- user agent plugin ([#503](https://github.com/amplitude/Amplitude-TypeScript/issues/503)) + ([d85e230](https://github.com/amplitude/Amplitude-TypeScript/commit/d85e230193b0c4286586fb23a8e020413051c595)) diff --git a/packages/plugin-user-agent-enrichment-browser/package.json b/packages/plugin-user-agent-enrichment-browser/package.json index 73bffb4e0..a801a263a 100644 --- a/packages/plugin-user-agent-enrichment-browser/package.json +++ b/packages/plugin-user-agent-enrichment-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-user-agent-enrichment-browser", - "version": "0.0.0", + "version": "0.1.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 6aeb511e5a7db760ebaa86aee0d7756fb85e9020 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Mon, 10 Jul 2023 10:33:05 -0400 Subject: [PATCH 066/214] feat(plugins): add default tracking of sessions to session replay plugin --- .../src/session-replay.ts | 16 +++++++++ .../src/typings/session-replay.ts | 1 + .../test/session-replay.test.ts | 36 +++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 8840f4352..10fafa8c9 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -42,6 +42,22 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { async setup(config: BrowserConfig) { config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); + if (typeof config.defaultTracking === 'boolean') { + if (config.defaultTracking === false) { + config.defaultTracking = { + pageViews: false, + formInteractions: false, + fileDownloads: false, + sessions: true, + }; + } + } else { + config.defaultTracking = { + ...config.defaultTracking, + sessions: true, + }; + } + this.config = config; this.storageKey = `${STORAGE_PREFIX}_${this.config.apiKey.substring(0, 10)}`; await this.initialize(true); diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index 823999f7d..a77c9f90c 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -32,6 +32,7 @@ export interface IDBStore { }; } export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { + setup: (config: BrowserConfig) => Promise; config: BrowserConfig; storageKey: string; retryTimeout: number; diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 98eb79cbe..553c69c49 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -293,6 +293,42 @@ describe('SessionReplayPlugin', () => { await sessionReplay.initialize(); expect(record).toHaveBeenCalledTimes(1); }); + describe('defaultTracking', () => { + test('should not change defaultTracking if its set to true', async () => { + const sessionReplay = sessionReplayPlugin(); + await sessionReplay.setup({ + ...mockConfig, + defaultTracking: true, + }); + expect(sessionReplay.config.defaultTracking).toBe(true); + }); + test('should modify defaultTracking to enable sessions if its set to false', async () => { + const sessionReplay = sessionReplayPlugin(); + await sessionReplay.setup({ + ...mockConfig, + defaultTracking: false, + }); + expect(sessionReplay.config.defaultTracking).toEqual({ + pageViews: false, + formInteractions: false, + fileDownloads: false, + sessions: true, + }); + }); + test('should modify defaultTracking to enable sessions if it is an object', async () => { + const sessionReplay = sessionReplayPlugin(); + await sessionReplay.setup({ + ...mockConfig, + defaultTracking: { + pageViews: false, + }, + }); + expect(sessionReplay.config.defaultTracking).toEqual({ + pageViews: false, + sessions: true, + }); + }); + }); }); describe('execute', () => { From bcabfa4ea784187edc85a85bce9a2c68dde411e5 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Mon, 10 Jul 2023 11:50:01 -0400 Subject: [PATCH 067/214] feat(plugins): add a configuration option for sampling rate --- .../plugin-session-replay-browser/README.md | 20 +++++-- .../src/session-replay.ts | 22 +++++++- .../src/typings/session-replay.ts | 11 ++-- .../test/session-replay.test.ts | 52 +++++++++++++++++++ 4 files changed, 94 insertions(+), 11 deletions(-) diff --git a/packages/plugin-session-replay-browser/README.md b/packages/plugin-session-replay-browser/README.md index 5f546afe4..2a3e9a11f 100644 --- a/packages/plugin-session-replay-browser/README.md +++ b/packages/plugin-session-replay-browser/README.md @@ -35,12 +35,26 @@ import * as amplitude from '@amplitude/analytics-browser'; import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'; ``` -### 2. Instantiate session replay plugin and install plugin to Amplitude SDK +### 2. Instantiate Session Replay plugin -The plugin must be registered with the amplitude instance via the following code: +The plugin must be registered with the amplitude instance via the following code. The plugin accepts an optional parameter which is an `Object` to configure the plugin based on your use case. ```typescript amplitude.init(API_KEY); -const sessionReplayTracking = sessionReplayPlugin(); +const sessionReplayTracking = sessionReplayPlugin({ + sampleRate: undefined +}); +``` + + +#### Options + +|Name|Type|Default|Description| +|-|-|-|-| +|`sampleRate`|`number`|`undefined`|Use this option to control how many sessions will be selected for recording.

The number should be a decimal between 0 and 1, ie `0.4`, representing the fraction of sessions you would like to have randomly selected for recording. Over a large number of sessions, `0.4` would select `40%` of those sessions.| + +### 3. Install plugin to Amplitude SDK + +```typescript amplitude.add(sessionReplayTracking); ``` \ No newline at end of file diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 10fafa8c9..525152d40 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -11,6 +11,7 @@ import { RecordingStatus, SessionReplayContext, SessionReplayEnrichmentPlugin, + SessionReplayOptions, SessionReplayPlugin, } from './typings/session-replay'; @@ -38,9 +39,22 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { maxPersistedEventsSize = MAX_EVENT_LIST_SIZE_IN_BYTES; interval = MIN_INTERVAL; timeAtLastSend: number | null = null; + options: SessionReplayOptions; + shouldRecordEvents = true; + + constructor(options?: SessionReplayOptions) { + this.options = { ...options }; + if (options?.sampleRate) { + this.shouldRecordEvents = Math.random() < options.sampleRate; + } + } async setup(config: BrowserConfig) { config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); + if (!this.shouldRecordEvents) { + config.loggerProvider.log('Opting session out of recording due to lack of inclusion in sample.'); + return; + } if (typeof config.defaultTracking === 'boolean') { if (config.defaultTracking === false) { @@ -75,6 +89,10 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } async execute(event: Event) { + if (!this.shouldRecordEvents) { + return Promise.resolve(event); + } + event.event_properties = { ...event.event_properties, [DEFAULT_SESSION_REPLAY_PROPERTY]: true, @@ -405,6 +423,6 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } } -export const sessionReplayPlugin: SessionReplayPlugin = () => { - return new SessionReplay(); +export const sessionReplayPlugin: SessionReplayPlugin = (options?: SessionReplayOptions) => { + return new SessionReplay(options); }; diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index a77c9f90c..354995682 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -1,8 +1,10 @@ -import { BrowserClient, BrowserConfig, EnrichmentPlugin } from '@amplitude/analytics-types'; +import { BrowserConfig, EnrichmentPlugin } from '@amplitude/analytics-types'; import { record } from 'rrweb'; // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface Options {} +export interface SessionReplayOptions { + sampleRate?: number; +} export type Events = string[]; @@ -76,8 +78,5 @@ export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { } export interface SessionReplayPlugin { - (client: BrowserClient, options?: Options): SessionReplayEnrichmentPlugin; - (options?: Options): SessionReplayEnrichmentPlugin; + (options?: SessionReplayOptions): SessionReplayEnrichmentPlugin; } - -export type SessionReplayPluginParameters = [BrowserClient, Options?] | [Options?]; diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 553c69c49..378dde624 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -748,6 +748,58 @@ describe('SessionReplayPlugin', () => { }); describe('module level integration', () => { + describe('with a sample rate', () => { + test('should not record session if excluded due to sampling', async () => { + jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); + const sessionReplay = sessionReplayPlugin({ + sampleRate: 0.2, + }); + await sessionReplay.setup(mockConfig); + await sessionReplay.execute({ + event_type: 'session_end', + session_id: 456, + }); + await runScheduleTimers(); + expect(record).not.toHaveBeenCalled(); + expect(fetch).not.toHaveBeenCalled(); + expect(update).not.toHaveBeenCalled(); + }); + test('should record session if included due to sampling', async () => { + (fetch as jest.Mock).mockImplementationOnce(() => { + return Promise.resolve({ + status: 200, + }); + }); + jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); + const sessionReplay = sessionReplayPlugin({ + sampleRate: 0.8, + }); + const config = { + ...mockConfig, + loggerProvider: mockLoggerProvider, + }; + await sessionReplay.setup(config); + // Log is called from setup, but that's not what we're testing here + mockLoggerProvider.log.mockClear(); + await sessionReplay.execute({ + event_type: 'session_start', + session_id: 456, + }); + expect(record).toHaveBeenCalled(); + const recordArg = record.mock.calls[0][0]; + recordArg?.emit && recordArg?.emit(mockEvent); + expect(update).toHaveBeenCalledTimes(1); + await sessionReplay.execute({ + event_type: 'session_end', + session_id: 456, + }); + await runScheduleTimers(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.log).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(mockLoggerProvider.log.mock.calls[0][0]).toEqual(SUCCESS_MESSAGE); + }); + }); test('should handle unexpected error', async () => { const sessionReplay = sessionReplayPlugin(); const config = { From 25bd516849c9a7fc346c6a868c664e70cdeb5cba Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Mon, 10 Jul 2023 12:22:58 -0400 Subject: [PATCH 068/214] feat(plugins): abide by global opt out for session recording --- .../src/session-replay.ts | 6 +++- .../test/session-replay.test.ts | 30 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 525152d40..9a4b475ec 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -51,8 +51,12 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { async setup(config: BrowserConfig) { config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); + if (config.optOut) { + this.shouldRecordEvents = false; + } + if (!this.shouldRecordEvents) { - config.loggerProvider.log('Opting session out of recording due to lack of inclusion in sample.'); + config.loggerProvider.log('Opting session out of recording.'); return; } diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 378dde624..c2cc372a2 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -755,14 +755,18 @@ describe('SessionReplayPlugin', () => { sampleRate: 0.2, }); await sessionReplay.setup(mockConfig); + await sessionReplay.execute({ + event_type: 'session_start', + session_id: 456, + }); + expect(record).not.toHaveBeenCalled(); + expect(update).not.toHaveBeenCalled(); await sessionReplay.execute({ event_type: 'session_end', session_id: 456, }); await runScheduleTimers(); - expect(record).not.toHaveBeenCalled(); expect(fetch).not.toHaveBeenCalled(); - expect(update).not.toHaveBeenCalled(); }); test('should record session if included due to sampling', async () => { (fetch as jest.Mock).mockImplementationOnce(() => { @@ -800,6 +804,28 @@ describe('SessionReplayPlugin', () => { expect(mockLoggerProvider.log.mock.calls[0][0]).toEqual(SUCCESS_MESSAGE); }); }); + + describe('with optOut in config', () => { + test('should not record session if excluded due to optOut', async () => { + const sessionReplay = sessionReplayPlugin(); + await sessionReplay.setup({ + ...mockConfig, + optOut: true, + }); + await sessionReplay.execute({ + event_type: 'session_start', + session_id: 456, + }); + expect(record).not.toHaveBeenCalled(); + expect(update).not.toHaveBeenCalled(); + await sessionReplay.execute({ + event_type: 'session_end', + session_id: 456, + }); + await runScheduleTimers(); + expect(fetch).not.toHaveBeenCalled(); + }); + }); test('should handle unexpected error', async () => { const sessionReplay = sessionReplayPlugin(); const config = { From 5c04c3cc0e8ef287898f2571f8c2a3e9e00311be Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Tue, 25 Jul 2023 19:35:03 -0700 Subject: [PATCH 069/214] feat(plugins): update config additions to store in idb --- .../src/messages.ts | 3 +- .../src/session-replay.ts | 88 +++-- .../src/typings/session-replay.ts | 16 +- .../test/session-replay.test.ts | 303 ++++++++++++++++-- 4 files changed, 340 insertions(+), 70 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/messages.ts b/packages/plugin-session-replay-browser/src/messages.ts index 8b93b23da..dd1de2e45 100644 --- a/packages/plugin-session-replay-browser/src/messages.ts +++ b/packages/plugin-session-replay-browser/src/messages.ts @@ -1,4 +1,5 @@ -export const SUCCESS_MESSAGE = 'Session replay event batch tracked successfully'; +export const getSuccessMessage = (sessionId: number) => + `Session replay event batch tracked successfully for session id ${sessionId}`; export const UNEXPECTED_ERROR_MESSAGE = 'Unexpected error occurred'; export const MAX_RETRIES_EXCEEDED_MESSAGE = 'Session replay event batch rejected due to exceeded retry count'; export const STORAGE_FAILURE = 'Failed to store session replay events in IndexedDB'; diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 9a4b475ec..a34c47c3f 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -4,10 +4,11 @@ import { BrowserConfig, Event, Status } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import { pack, record } from 'rrweb'; import { DEFAULT_SESSION_END_EVENT, DEFAULT_SESSION_REPLAY_PROPERTY, DEFAULT_SESSION_START_EVENT } from './constants'; -import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from './messages'; +import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, UNEXPECTED_ERROR_MESSAGE, getSuccessMessage } from './messages'; import { Events, IDBStore, + IDBStoreSession, RecordingStatus, SessionReplayContext, SessionReplayEnrichmentPlugin, @@ -21,7 +22,11 @@ const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 500; // derived by JSON s const MAX_EVENT_LIST_SIZE_IN_BYTES = 10 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; const MIN_INTERVAL = 500; // 500 ms const MAX_INTERVAL = 10 * 1000; // 10 seconds - +const defaultSessionStore: IDBStoreSession = { + shouldRecord: true, + currentSequenceId: 0, + sessionSequences: {}, +}; class SessionReplay implements SessionReplayEnrichmentPlugin { name = '@amplitude/plugin-session-replay-browser'; type = 'enrichment' as const; @@ -40,25 +45,18 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { interval = MIN_INTERVAL; timeAtLastSend: number | null = null; options: SessionReplayOptions; - shouldRecordEvents = true; + shouldRecord = true; constructor(options?: SessionReplayOptions) { this.options = { ...options }; - if (options?.sampleRate) { - this.shouldRecordEvents = Math.random() < options.sampleRate; - } } async setup(config: BrowserConfig) { config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); - if (config.optOut) { - this.shouldRecordEvents = false; - } - if (!this.shouldRecordEvents) { - config.loggerProvider.log('Opting session out of recording.'); - return; - } + this.config = config; + this.config.sessionId = config.sessionId; + this.storageKey = `${STORAGE_PREFIX}_${this.config.apiKey.substring(0, 10)}`; if (typeof config.defaultTracking === 'boolean') { if (config.defaultTracking === false) { @@ -76,8 +74,6 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { }; } - this.config = config; - this.storageKey = `${STORAGE_PREFIX}_${this.config.apiKey.substring(0, 10)}`; await this.initialize(true); const GlobalScope = getGlobalScope(); @@ -93,18 +89,17 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } async execute(event: Event) { - if (!this.shouldRecordEvents) { - return Promise.resolve(event); + if (this.shouldRecord) { + event.event_properties = { + ...event.event_properties, + [DEFAULT_SESSION_REPLAY_PROPERTY]: true, + }; } - - event.event_properties = { - ...event.event_properties, - [DEFAULT_SESSION_REPLAY_PROPERTY]: true, - }; if (event.event_type === DEFAULT_SESSION_START_EVENT && !this.stopRecordingEvents) { + this.setShouldRecord(); this.recordEvents(); } else if (event.event_type === DEFAULT_SESSION_END_EVENT) { - if (event.session_id) { + if (event.session_id && this.events.length) { this.sendEventsList({ events: this.events, sequenceId: this.currentSequenceId, @@ -129,15 +124,16 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { if (storedReplaySessions && storedSequencesForSession && storedSequencesForSession.sessionSequences) { const storedSeqId = storedSequencesForSession.currentSequenceId; const lastSequence = storedSequencesForSession.sessionSequences[storedSeqId]; - if (lastSequence.status !== RecordingStatus.RECORDING) { + if (lastSequence && lastSequence.status !== RecordingStatus.RECORDING) { this.currentSequenceId = storedSeqId + 1; this.events = []; } else { // Pick up recording where it was left off in another tab or window this.currentSequenceId = storedSeqId; - this.events = lastSequence.events; + this.events = lastSequence?.events || []; } } + this.setShouldRecord(storedSequencesForSession); if (shouldSendStoredEvents && storedReplaySessions) { this.sendStoredEvents(storedReplaySessions); } @@ -146,6 +142,20 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } } + setShouldRecord(sessionStore?: IDBStoreSession) { + if (sessionStore?.shouldRecord === false) { + this.shouldRecord = false; + } else if (this.options && this.options.sampleRate) { + this.shouldRecord = Math.random() < this.options.sampleRate; + } else if (this.config.optOut) { + this.shouldRecord = false; + } + this.config.sessionId && void this.storeShouldRecordForSession(this.config.sessionId, this.shouldRecord); + if (!this.shouldRecord && this.config.sessionId) { + this.config.loggerProvider.log(`Opting session ${this.config.sessionId} out of recording.`); + } + } + sendStoredEvents(storedReplaySessions: IDBStore) { for (const sessionId in storedReplaySessions) { const storedSequences = storedReplaySessions[sessionId].sessionSequences; @@ -168,6 +178,9 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } recordEvents() { + if (!this.shouldRecord) { + return; + } this.stopRecordingEvents = record({ emit: (event) => { const GlobalScope = getGlobalScope(); @@ -330,7 +343,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } handleSuccessResponse(context: SessionReplayContext) { - this.completeRequest({ context, success: SUCCESS_MESSAGE }); + this.completeRequest({ context, success: getSuccessMessage(context.sessionId) }); } handleOtherResponse(context: SessionReplayContext) { @@ -354,10 +367,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { async storeEventsForSession(events: Events, sequenceId: number, sessionId: number) { try { await IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore = {}): IDBStore => { - const session = sessionMap[sessionId] || { - currentSequenceId: 0, - sessionSequences: [], - }; + const session: IDBStoreSession = sessionMap[sessionId] || { ...defaultSessionStore }; session.currentSequenceId = sequenceId; const currentSequence = (session.sessionSequences && session.sessionSequences[sequenceId]) || {}; @@ -381,10 +391,26 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } } + async storeShouldRecordForSession(sessionId: number, shouldRecord: boolean) { + try { + await IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore = {}): IDBStore => { + const session: IDBStoreSession = sessionMap[sessionId] || { ...defaultSessionStore }; + session.shouldRecord = shouldRecord; + + return { + ...sessionMap, + [sessionId]: session, + }; + }); + } catch (e) { + this.config.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); + } + } + async cleanUpSessionEventsStore(sessionId: number, sequenceId: number) { try { await IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore = {}): IDBStore => { - const session = sessionMap[sessionId]; + const session: IDBStoreSession = sessionMap[sessionId]; const sequenceToUpdate = session?.sessionSequences && session.sessionSequences[sequenceId]; if (!sequenceToUpdate) { return sessionMap; diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index 354995682..84f959410 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -27,12 +27,17 @@ export interface IDBStoreSequence { status: RecordingStatus; } -export interface IDBStore { - [sessionId: number]: { - currentSequenceId: number; - sessionSequences: { [sequenceId: number]: IDBStoreSequence }; +export interface IDBStoreSession { + shouldRecord: boolean; + currentSequenceId: number; + sessionSequences: { + [sequenceId: number]: IDBStoreSequence; }; } + +export interface IDBStore { + [sessionId: number]: IDBStoreSession; +} export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { setup: (config: BrowserConfig) => Promise; config: BrowserConfig; @@ -41,11 +46,13 @@ export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { events: Events; currentSequenceId: number; interval: number; + shouldRecord: boolean; queue: SessionReplayContext[]; timeAtLastSend: number | null; stopRecordingEvents: ReturnType | null; maxPersistedEventsSize: number; initialize: (shouldSendStoredEvents?: boolean) => Promise; + setShouldRecord: (sessionStore?: IDBStoreSession) => void; recordEvents: () => void; shouldSplitEventsList: (nextEventString: string) => boolean; sendEventsList: ({ @@ -74,6 +81,7 @@ export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { }): void; getAllSessionEventsFromStore: () => Promise; storeEventsForSession: (events: Events, sequenceId: number, sessionId: number) => Promise; + storeShouldRecordForSession: (sessionId: number, shouldRecord: boolean) => Promise; cleanUpSessionEventsStore: (sessionId: number, sequenceId: number) => Promise; } diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index c2cc372a2..ccdffd601 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -1,10 +1,10 @@ /* eslint-disable jest/expect-expect */ import * as AnalyticsClientCommon from '@amplitude/analytics-client-common'; import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; -import { BrowserConfig, LogLevel } from '@amplitude/analytics-types'; +import { BrowserConfig, LogLevel, Logger } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import * as RRWeb from 'rrweb'; -import { SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE } from '../src/messages'; +import { UNEXPECTED_ERROR_MESSAGE, getSuccessMessage } from '../src/messages'; import { sessionReplayPlugin } from '../src/session-replay'; import { IDBStore, RecordingStatus } from '../src/typings/session-replay'; @@ -14,6 +14,8 @@ type MockedIDBKeyVal = jest.Mocked; jest.mock('rrweb'); type MockedRRWeb = jest.Mocked; +type MockedLogger = jest.Mocked; + const mockEvent = { type: 4, data: { href: 'https://analytics.amplitude.com/', width: 1728, height: 154 }, @@ -30,20 +32,11 @@ async function runScheduleTimers() { jest.runAllTimers(); } -const mockLoggerProvider = { - error: jest.fn(), - log: jest.fn(), - logLevel: 1, - disable: jest.fn(), - enable: jest.fn(), - warn: jest.fn(), - debug: jest.fn(), -}; - describe('SessionReplayPlugin', () => { const { get, update } = IDBKeyVal as MockedIDBKeyVal; const { record } = RRWeb as MockedRRWeb; let originalFetch: typeof global.fetch; + let mockLoggerProvider: MockedLogger; let addEventListenerMock: jest.Mock; const mockConfig: BrowserConfig = { apiKey: 'static_key', @@ -78,10 +71,8 @@ describe('SessionReplayPlugin', () => { platform: true, }, }; - beforeAll(() => { - jest.useFakeTimers(); - }); beforeEach(() => { + jest.useFakeTimers(); originalFetch = global.fetch; global.fetch = jest.fn(() => Promise.resolve({ @@ -97,12 +88,19 @@ describe('SessionReplayPlugin', () => { hasFocus: () => true, }, } as Window & typeof globalThis); + mockLoggerProvider = { + error: jest.fn(), + log: jest.fn(), + disable: jest.fn(), + enable: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }; }); afterEach(() => { jest.resetAllMocks(); + jest.spyOn(global.Math, 'random').mockRestore(); global.fetch = originalFetch; - }); - afterAll(() => { jest.useRealTimers(); }); describe('setup', () => { @@ -165,6 +163,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = config; const mockGetResolution: Promise = Promise.resolve({ 123: { + shouldRecord: true, currentSequenceId: 3, sessionSequences: { 3: { @@ -174,6 +173,7 @@ describe('SessionReplayPlugin', () => { }, }, 456: { + shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -243,6 +243,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockGetResolution: Promise = Promise.resolve({ 123: { + shouldRecord: true, currentSequenceId: 3, sessionSequences: { 3: { @@ -262,6 +263,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockGetResolution: Promise = Promise.resolve({ 123: { + shouldRecord: true, currentSequenceId: 3, sessionSequences: { 3: { @@ -285,6 +287,21 @@ describe('SessionReplayPlugin', () => { expect(sessionReplay.currentSequenceId).toBe(0); expect(sessionReplay.events).toEqual([]); }); + test('should handle no stored sequences for session', async () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + const mockGetResolution = Promise.resolve({ + 123: { + shouldRecord: true, + currentSequenceId: 0, + sessionSequences: {}, + }, + }); + get.mockReturnValueOnce(mockGetResolution); + await sessionReplay.initialize(); + expect(sessionReplay.currentSequenceId).toBe(0); + expect(sessionReplay.events).toEqual([]); + }); test('should record events', async () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; @@ -331,6 +348,47 @@ describe('SessionReplayPlugin', () => { }); }); + describe('setShouldRecord', () => { + test('should set record as false if false in session store', () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + const sessionStore = { + shouldRecord: false, + currentSequenceId: 0, + sessionSequences: {}, + }; + expect(sessionReplay.shouldRecord).toBe(true); + sessionReplay.setShouldRecord(sessionStore); + expect(sessionReplay.shouldRecord).toBe(false); + }); + test('should not set record as false if no options', () => { + jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + sessionReplay.setShouldRecord(); + expect(sessionReplay.shouldRecord).toBe(true); + }); + test('should set record as false if session not included in sample rate', () => { + jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); + const sessionReplay = sessionReplayPlugin({ + sampleRate: 0.2, + }); + sessionReplay.config = mockConfig; + expect(sessionReplay.shouldRecord).toBe(true); + sessionReplay.setShouldRecord(); + expect(sessionReplay.shouldRecord).toBe(false); + }); + test('should set record as false if opt out in config', () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = { + ...mockConfig, + optOut: true, + }; + sessionReplay.setShouldRecord(); + expect(sessionReplay.shouldRecord).toBe(false); + }); + }); + describe('execute', () => { test('should add event property for [Amplitude] Session Recorded', async () => { const sessionReplay = sessionReplayPlugin(); @@ -406,6 +464,7 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1]({})).toEqual({ 123: { + shouldRecord: true, currentSequenceId: 0, sessionSequences: { 0: { @@ -420,16 +479,16 @@ describe('SessionReplayPlugin', () => { test('should split the events list at an increasing interval and send', () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; + sessionReplay.timeAtLastSend = new Date('2023-07-31 08:30:00').getTime(); sessionReplay.recordEvents(); - sessionReplay.timeAtLastSend = 1; - const dateNowMock = jest.spyOn(Date, 'now').mockReturnValue(1); + jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:30:00').getTime()); const sendEventsList = jest.spyOn(sessionReplay, 'sendEventsList'); const recordArg = record.mock.calls[0][0]; // Emit first event, which is not sent immediately recordArg?.emit && recordArg?.emit(mockEvent); expect(sendEventsList).toHaveBeenCalledTimes(0); // Emit second event and advance timers to interval - dateNowMock.mockReturnValue(1002); + jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:31:00').getTime()); recordArg?.emit && recordArg?.emit(mockEvent); expect(sendEventsList).toHaveBeenCalledTimes(1); expect(sendEventsList).toHaveBeenCalledWith({ @@ -439,7 +498,6 @@ describe('SessionReplayPlugin', () => { }); expect(sessionReplay.events).toEqual([mockEventString]); expect(sessionReplay.currentSequenceId).toEqual(1); - dateNowMock.mockClear(); }); test('should split the events list at max size and send', () => { @@ -467,6 +525,7 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1]({})).toEqual({ 123: { + shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -755,19 +814,93 @@ describe('SessionReplayPlugin', () => { sampleRate: 0.2, }); await sessionReplay.setup(mockConfig); + expect(update).toHaveBeenCalled(); + expect(update.mock.calls[0][1]({})).toEqual({ + 123: { + shouldRecord: false, + currentSequenceId: 0, + sessionSequences: {}, + }, + }); + update.mockClear(); await sessionReplay.execute({ - event_type: 'session_start', - session_id: 456, + event_type: 'my_event', + session_id: 123, }); expect(record).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); await sessionReplay.execute({ event_type: 'session_end', - session_id: 456, + session_id: 123, + }); + await runScheduleTimers(); + expect(fetch).not.toHaveBeenCalled(); + }); + test('should recalculate whether to exclude session due to sample rate when start session fires', async () => { + jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.1); + const sessionReplay = sessionReplayPlugin({ + sampleRate: 0.2, + }); + await sessionReplay.setup(mockConfig); + expect(update).toHaveBeenCalled(); + expect(update.mock.calls[0][1]({})).toEqual({ + 123: { + shouldRecord: true, + currentSequenceId: 0, + sessionSequences: {}, + }, + }); + update.mockClear(); + // Record is called in setup, but we're not interested in that now + record.mockClear(); + // This will exclude session from sample rate + jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); + await sessionReplay.execute({ + event_type: 'session_start', + session_id: 123, + }); + expect(record).not.toHaveBeenCalled(); + expect(update).toHaveBeenCalled(); + expect(update.mock.calls[0][1]({})).toEqual({ + 123: { + shouldRecord: false, + currentSequenceId: 0, + sessionSequences: {}, + }, + }); + await sessionReplay.execute({ + event_type: 'session_end', + session_id: 123, }); await runScheduleTimers(); expect(fetch).not.toHaveBeenCalled(); }); + test('should fetch whether to record session from IDB', async () => { + const sessionReplay = sessionReplayPlugin(); + const mockIDBStore = { + 123: { + shouldRecord: false, + currentSequenceId: 0, + sessionSequences: {}, + }, + }; + get.mockImplementationOnce(() => Promise.resolve(mockIDBStore)); + await sessionReplay.setup(mockConfig); + expect(record).not.toHaveBeenCalled(); + expect(update).toHaveBeenCalled(); + expect(update.mock.calls[0][1]({})).toEqual({ + 123: { + shouldRecord: false, + currentSequenceId: 0, + sessionSequences: {}, + }, + }); + await sessionReplay.execute({ + event_type: 'session_start', + session_id: 123, + }); + expect(record).not.toHaveBeenCalled(); + }); test('should record session if included due to sampling', async () => { (fetch as jest.Mock).mockImplementationOnce(() => { return Promise.resolve({ @@ -783,25 +916,25 @@ describe('SessionReplayPlugin', () => { loggerProvider: mockLoggerProvider, }; await sessionReplay.setup(config); - // Log is called from setup, but that's not what we're testing here - mockLoggerProvider.log.mockClear(); await sessionReplay.execute({ event_type: 'session_start', session_id: 456, }); + // Log is called from setup, but that's not what we're testing here + mockLoggerProvider.log.mockClear(); expect(record).toHaveBeenCalled(); const recordArg = record.mock.calls[0][0]; recordArg?.emit && recordArg?.emit(mockEvent); - expect(update).toHaveBeenCalledTimes(1); await sessionReplay.execute({ event_type: 'session_end', session_id: 456, }); await runScheduleTimers(); expect(fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockLoggerProvider.log).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.log.mock.calls[0][0]).toEqual(SUCCESS_MESSAGE); + expect(mockLoggerProvider.log.mock.calls[0][0]).toEqual(getSuccessMessage(456)); }); }); @@ -817,7 +950,6 @@ describe('SessionReplayPlugin', () => { session_id: 456, }); expect(record).not.toHaveBeenCalled(); - expect(update).not.toHaveBeenCalled(); await sessionReplay.execute({ event_type: 'session_end', session_id: 456, @@ -834,12 +966,14 @@ describe('SessionReplayPlugin', () => { }; (fetch as jest.Mock).mockImplementationOnce(() => Promise.reject('API Failure')); await sessionReplay.setup(config); + sessionReplay.events = [mockEventString]; await sessionReplay.execute({ event_type: 'session_end', session_id: 456, }); await runScheduleTimers(); expect(fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual('API Failure'); @@ -866,16 +1000,19 @@ describe('SessionReplayPlugin', () => { await sessionReplay.setup(config); // Log is called from setup, but that's not what we're testing here mockLoggerProvider.log.mockClear(); + sessionReplay.events = [mockEventString]; await sessionReplay.execute({ event_type: 'session_end', session_id: 456, }); await runScheduleTimers(); expect(fetch).toHaveBeenCalledTimes(2); + // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockLoggerProvider.error).toHaveBeenCalledTimes(0); + // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockLoggerProvider.log).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.log.mock.calls[0][0]).toEqual(SUCCESS_MESSAGE); + expect(mockLoggerProvider.log.mock.calls[0][0]).toEqual(getSuccessMessage(456)); }); test('should handle retry for 413 error with flushQueueSize of 1', async () => { (fetch as jest.Mock) @@ -895,6 +1032,7 @@ describe('SessionReplayPlugin', () => { flushMaxRetries: 2, }; await sessionReplay.setup(config); + sessionReplay.events = [mockEventString]; const event = { event_type: 'session_end', session_id: 456, @@ -921,6 +1059,7 @@ describe('SessionReplayPlugin', () => { flushMaxRetries: 2, }; await sessionReplay.setup(config); + sessionReplay.events = [mockEventString]; const event = { event_type: 'session_end', session_id: 456, @@ -947,6 +1086,7 @@ describe('SessionReplayPlugin', () => { flushMaxRetries: 2, }; await sessionReplay.setup(config); + sessionReplay.events = [mockEventString]; const event = { event_type: 'session_end', session_id: 456, @@ -966,6 +1106,7 @@ describe('SessionReplayPlugin', () => { loggerProvider: mockLoggerProvider, }; await sessionReplay.setup(config); + sessionReplay.events = [mockEventString]; const event = { event_type: 'session_end', session_id: 456, @@ -973,6 +1114,7 @@ describe('SessionReplayPlugin', () => { await sessionReplay.execute(event); await runScheduleTimers(); expect(fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual(UNEXPECTED_ERROR_MESSAGE); @@ -989,6 +1131,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = config; get.mockImplementationOnce(() => Promise.reject('error')); await sessionReplay.getAllSessionEventsFromStore(); + // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( @@ -996,6 +1139,75 @@ describe('SessionReplayPlugin', () => { ); }); }); + describe('storeShouldRecordForSession', () => { + test('should store if should record session', async () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + const mockIDBStore: IDBStore = { + 123: { + shouldRecord: true, + currentSequenceId: 3, + sessionSequences: { + 2: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, + }, + 456: { + shouldRecord: true, + currentSequenceId: 1, + sessionSequences: { + 1: { + events: [], + status: RecordingStatus.SENT, + }, + }, + }, + }; + await sessionReplay.storeShouldRecordForSession(456, false); + expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ + ...mockIDBStore, + 456: { + ...mockIDBStore[456], + shouldRecord: false, + }, + }); + }); + test('should catch errors', async () => { + const sessionReplay = sessionReplayPlugin(); + const config = { + ...mockConfig, + loggerProvider: mockLoggerProvider, + }; + sessionReplay.config = config; + update.mockImplementationOnce(() => Promise.reject('error')); + await sessionReplay.storeShouldRecordForSession(123, true); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( + 'Failed to store session replay events in IndexedDB: error', + ); + }); + test('should handle an undefined store', async () => { + const sessionReplay = sessionReplayPlugin(); + const config = { + ...mockConfig, + loggerProvider: mockLoggerProvider, + }; + sessionReplay.config = config; + update.mockImplementationOnce(() => Promise.resolve()); + await sessionReplay.storeShouldRecordForSession(123, true); + expect(update.mock.calls[0][1](undefined)).toEqual({ + 123: { + shouldRecord: true, + currentSequenceId: 0, + sessionSequences: {}, + }, + }); + }); + }); describe('cleanUpSessionEventsStore', () => { test('should update events and status for current session', async () => { @@ -1003,6 +1215,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockIDBStore: IDBStore = { 123: { + shouldRecord: true, currentSequenceId: 3, sessionSequences: { 2: { @@ -1016,6 +1229,7 @@ describe('SessionReplayPlugin', () => { }, }, 456: { + shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1030,6 +1244,7 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ 123: { + shouldRecord: true, currentSequenceId: 3, sessionSequences: { 2: { @@ -1043,6 +1258,7 @@ describe('SessionReplayPlugin', () => { }, }, 456: { + shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1059,6 +1275,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockIDBStore: IDBStore = { 123: { + shouldRecord: true, currentSequenceId: 3, sessionSequences: { 2: { @@ -1072,6 +1289,7 @@ describe('SessionReplayPlugin', () => { }, }, 456: { + shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1086,6 +1304,7 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ 123: { + shouldRecord: true, currentSequenceId: 3, sessionSequences: { 3: { @@ -1095,6 +1314,7 @@ describe('SessionReplayPlugin', () => { }, }, 456: { + shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1114,6 +1334,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = config; update.mockImplementationOnce(() => Promise.reject('error')); await sessionReplay.cleanUpSessionEventsStore(123, 1); + // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( @@ -1139,6 +1360,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockIDBStore: IDBStore = { 123: { + shouldRecord: true, currentSequenceId: 2, sessionSequences: { 2: { @@ -1148,6 +1370,7 @@ describe('SessionReplayPlugin', () => { }, }, 456: { + shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1162,6 +1385,7 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ 123: { + shouldRecord: true, currentSequenceId: 2, sessionSequences: { 2: { @@ -1171,6 +1395,7 @@ describe('SessionReplayPlugin', () => { }, }, 456: { + shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1186,6 +1411,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockIDBStore: IDBStore = { 123: { + shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1195,6 +1421,7 @@ describe('SessionReplayPlugin', () => { }, }, 456: { + shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1209,6 +1436,7 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ 123: { + shouldRecord: true, currentSequenceId: 2, sessionSequences: { 1: { @@ -1222,6 +1450,7 @@ describe('SessionReplayPlugin', () => { }, }, 456: { + shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1237,6 +1466,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockIDBStore: IDBStore = { 123: { + shouldRecord: true, currentSequenceId: 2, sessionSequences: { 2: { @@ -1251,6 +1481,7 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ 123: { + shouldRecord: true, currentSequenceId: 2, sessionSequences: { 2: { @@ -1260,6 +1491,7 @@ describe('SessionReplayPlugin', () => { }, }, 456: { + shouldRecord: true, currentSequenceId: 0, sessionSequences: { 0: { @@ -1279,6 +1511,8 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = config; update.mockImplementationOnce(() => Promise.reject('error')); await sessionReplay.storeEventsForSession([mockEventString], 0, config.sessionId as number); + + // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( @@ -1296,6 +1530,7 @@ describe('SessionReplayPlugin', () => { await sessionReplay.storeEventsForSession([mockEventString], 0, 456); expect(update.mock.calls[0][1](undefined)).toEqual({ 456: { + shouldRecord: true, currentSequenceId: 0, sessionSequences: { 0: { @@ -1338,17 +1573,17 @@ describe('SessionReplayPlugin', () => { }); test('should return false if it has not been long enough since last send', () => { const sessionReplay = sessionReplayPlugin(); - sessionReplay.timeAtLastSend = 1; - jest.spyOn(Date, 'now').mockReturnValue(2); + sessionReplay.timeAtLastSend = new Date('2023-07-31 08:30:00').getTime(); + jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:30:00').getTime()); const nextEvent = 'a'; const result = sessionReplay.shouldSplitEventsList(nextEvent); expect(result).toBe(false); }); test('should return true if it has been long enough since last send and events have been emitted', () => { const sessionReplay = sessionReplayPlugin(); - sessionReplay.timeAtLastSend = 1; sessionReplay.events = [mockEventString]; - jest.spyOn(Date, 'now').mockReturnValue(100002); + sessionReplay.timeAtLastSend = new Date('2023-07-31 08:30:00').getTime(); + jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:33:00').getTime()); const nextEvent = 'a'; const result = sessionReplay.shouldSplitEventsList(nextEvent); expect(result).toBe(true); From 5438a438ec062b3011234b0fea82b54351a12864 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Wed, 26 Jul 2023 14:10:11 -0700 Subject: [PATCH 070/214] fix(plugins): ensure config.optout takes priority over sample rate --- .../plugin-session-replay-browser/README.md | 2 +- .../src/session-replay.ts | 5 +++-- .../test/session-replay.test.ts | 22 ++++++++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/plugin-session-replay-browser/README.md b/packages/plugin-session-replay-browser/README.md index 2a3e9a11f..9b0fdfcf5 100644 --- a/packages/plugin-session-replay-browser/README.md +++ b/packages/plugin-session-replay-browser/README.md @@ -51,7 +51,7 @@ const sessionReplayTracking = sessionReplayPlugin({ |Name|Type|Default|Description| |-|-|-|-| -|`sampleRate`|`number`|`undefined`|Use this option to control how many sessions will be selected for recording.

The number should be a decimal between 0 and 1, ie `0.4`, representing the fraction of sessions you would like to have randomly selected for recording. Over a large number of sessions, `0.4` would select `40%` of those sessions.| +|`sampleRate`|`number`|`undefined`|Use this option to control how many sessions will be selected for recording. A selected session will be recorded, while sessions that are not selected will not be recorded.

The number should be a decimal between 0 and 1, ie `0.4`, representing the fraction of sessions you would like to have randomly selected for recording. Over a large number of sessions, `0.4` would select `40%` of those sessions.| ### 3. Install plugin to Amplitude SDK diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index a34c47c3f..b6f2f8cea 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -145,11 +145,12 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { setShouldRecord(sessionStore?: IDBStoreSession) { if (sessionStore?.shouldRecord === false) { this.shouldRecord = false; - } else if (this.options && this.options.sampleRate) { - this.shouldRecord = Math.random() < this.options.sampleRate; } else if (this.config.optOut) { this.shouldRecord = false; + } else if (this.options && this.options.sampleRate) { + this.shouldRecord = Math.random() < this.options.sampleRate; } + this.config.sessionId && void this.storeShouldRecordForSession(this.config.sessionId, this.shouldRecord); if (!this.shouldRecord && this.config.sessionId) { this.config.loggerProvider.log(`Opting session ${this.config.sessionId} out of recording.`); diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index ccdffd601..3c9615ef1 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -362,7 +362,6 @@ describe('SessionReplayPlugin', () => { expect(sessionReplay.shouldRecord).toBe(false); }); test('should not set record as false if no options', () => { - jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; sessionReplay.setShouldRecord(); @@ -378,6 +377,15 @@ describe('SessionReplayPlugin', () => { sessionReplay.setShouldRecord(); expect(sessionReplay.shouldRecord).toBe(false); }); + test('should set record as true if session is included in sample rate', () => { + jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); + const sessionReplay = sessionReplayPlugin({ + sampleRate: 0.8, + }); + sessionReplay.config = mockConfig; + sessionReplay.setShouldRecord(); + expect(sessionReplay.shouldRecord).toBe(true); + }); test('should set record as false if opt out in config', () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = { @@ -387,6 +395,18 @@ describe('SessionReplayPlugin', () => { sessionReplay.setShouldRecord(); expect(sessionReplay.shouldRecord).toBe(false); }); + test('opt out in config should override the sample rate', () => { + jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); + const sessionReplay = sessionReplayPlugin({ + sampleRate: 0.8, + }); + sessionReplay.config = { + ...mockConfig, + optOut: true, + }; + sessionReplay.setShouldRecord(); + expect(sessionReplay.shouldRecord).toBe(false); + }); }); describe('execute', () => { From ed20cf22180b33f29fd71a1439de26706d6ec4ff Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Wed, 26 Jul 2023 14:53:28 -0700 Subject: [PATCH 071/214] fix(plugins): update README for session replay --- packages/plugin-session-replay-browser/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/plugin-session-replay-browser/README.md b/packages/plugin-session-replay-browser/README.md index 9b0fdfcf5..0ada5f891 100644 --- a/packages/plugin-session-replay-browser/README.md +++ b/packages/plugin-session-replay-browser/README.md @@ -25,6 +25,8 @@ yarn add @amplitude/plugin-session-replay-browser This plugin works on top of Amplitude Browser SDK and adds session replay features to built-in features. To use this plugin, you need to install `@amplitude/analytics-browser` version `v1.0.0` or later. +This plugin requires that default tracking for sessions is enabled. If default tracking for sessions is not enabled in the config, the plugin will automatically enable it. + ### 1. Import Amplitude packages * `@amplitude/analytics-browser` From e46a42e430c7560a4e9da322b10238fdbf4f1bc5 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Wed, 26 Jul 2023 17:13:03 -0700 Subject: [PATCH 072/214] feat(plugins): add masking controls to session replay and reorganize constants and helper fns --- .../plugin-session-replay-browser/README.md | 12 +- .../package.json | 2 +- .../src/constants.ts | 17 +++ .../src/helpers.ts | 8 ++ .../src/session-replay.ts | 30 ++--- .../src/typings/session-replay.ts | 1 - .../test/helpers.test.ts | 24 ++++ yarn.lock | 103 ++++++++++++++---- 8 files changed, 158 insertions(+), 39 deletions(-) create mode 100644 packages/plugin-session-replay-browser/src/helpers.ts create mode 100644 packages/plugin-session-replay-browser/test/helpers.test.ts diff --git a/packages/plugin-session-replay-browser/README.md b/packages/plugin-session-replay-browser/README.md index 0ada5f891..66dd0b95d 100644 --- a/packages/plugin-session-replay-browser/README.md +++ b/packages/plugin-session-replay-browser/README.md @@ -59,4 +59,14 @@ const sessionReplayTracking = sessionReplayPlugin({ ```typescript amplitude.add(sessionReplayTracking); -``` \ No newline at end of file +``` + +## Privacy +By default, the session recording will mask all inputs, meaning the text in inputs will appear in a session replay as asterisks: `***`. You may require more specific masking controls based on your use case, so we offer the following controls: + +#### 1. Unmask inputs +In your application code, add the class `.amp-unmask` to any __input__ whose text you'd like to have unmasked in the recording. In the replay of a recorded session, it will be possible to read the exact text entered into an input with this class, the text will not be converted to asterisks. + +#### 2. Mask non-input elements +In your application code, add the class `.amp-mask` to any __non-input element__ whose text you'd like to have masked from the recording. The text in the element, as well as it's children, will all be converted to asterisks. + diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index f2a16b50c..0d0902019 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -39,7 +39,7 @@ "dependencies": { "@amplitude/analytics-types": ">=1 <3", "idb-keyval": "^6.2.1", - "rrweb": "^2.0.0-alpha.4", + "rrweb": "^2.0.0-alpha.9", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/plugin-session-replay-browser/src/constants.ts b/packages/plugin-session-replay-browser/src/constants.ts index 2ca6feab9..d726dbc10 100644 --- a/packages/plugin-session-replay-browser/src/constants.ts +++ b/packages/plugin-session-replay-browser/src/constants.ts @@ -1,5 +1,22 @@ +import { AMPLITUDE_PREFIX } from '@amplitude/analytics-core'; +import { IDBStoreSession } from './typings/session-replay'; + export const DEFAULT_EVENT_PROPERTY_PREFIX = '[Amplitude]'; export const DEFAULT_SESSION_REPLAY_PROPERTY = `${DEFAULT_EVENT_PROPERTY_PREFIX} Session Recorded`; export const DEFAULT_SESSION_START_EVENT = 'session_start'; export const DEFAULT_SESSION_END_EVENT = 'session_end'; + +export const MASK_TEXT_CLASS = 'amp-mask'; +export const UNMASK_TEXT_CLASS = 'amp-unmask'; +export const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/track'; +export const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; +const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 500; // derived by JSON stringifying an example payload without events +export const MAX_EVENT_LIST_SIZE_IN_BYTES = 10 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; +export const MIN_INTERVAL = 500; // 500 ms +export const MAX_INTERVAL = 10 * 1000; // 10 seconds +export const defaultSessionStore: IDBStoreSession = { + shouldRecord: true, + currentSequenceId: 0, + sessionSequences: {}, +}; diff --git a/packages/plugin-session-replay-browser/src/helpers.ts b/packages/plugin-session-replay-browser/src/helpers.ts new file mode 100644 index 000000000..3aeaaaf12 --- /dev/null +++ b/packages/plugin-session-replay-browser/src/helpers.ts @@ -0,0 +1,8 @@ +import { UNMASK_TEXT_CLASS } from './constants'; + +export const maskInputFn = (text: string, element: HTMLElement) => { + if (element.classList?.contains(UNMASK_TEXT_CLASS)) { + return text; + } + return '*'.repeat(text.length); +}; diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index b6f2f8cea..31b62be68 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -1,9 +1,21 @@ import { getGlobalScope } from '@amplitude/analytics-client-common'; -import { AMPLITUDE_PREFIX, BaseTransport } from '@amplitude/analytics-core'; +import { BaseTransport } from '@amplitude/analytics-core'; import { BrowserConfig, Event, Status } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import { pack, record } from 'rrweb'; -import { DEFAULT_SESSION_END_EVENT, DEFAULT_SESSION_REPLAY_PROPERTY, DEFAULT_SESSION_START_EVENT } from './constants'; +import { + DEFAULT_SESSION_END_EVENT, + DEFAULT_SESSION_REPLAY_PROPERTY, + DEFAULT_SESSION_START_EVENT, + MASK_TEXT_CLASS, + MAX_EVENT_LIST_SIZE_IN_BYTES, + MAX_INTERVAL, + MIN_INTERVAL, + SESSION_REPLAY_SERVER_URL, + STORAGE_PREFIX, + defaultSessionStore, +} from './constants'; +import { maskInputFn } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, UNEXPECTED_ERROR_MESSAGE, getSuccessMessage } from './messages'; import { Events, @@ -15,18 +27,6 @@ import { SessionReplayOptions, SessionReplayPlugin, } from './typings/session-replay'; - -const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/track'; -const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; -const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 500; // derived by JSON stringifying an example payload without events -const MAX_EVENT_LIST_SIZE_IN_BYTES = 10 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; -const MIN_INTERVAL = 500; // 500 ms -const MAX_INTERVAL = 10 * 1000; // 10 seconds -const defaultSessionStore: IDBStoreSession = { - shouldRecord: true, - currentSequenceId: 0, - sessionSequences: {}, -}; class SessionReplay implements SessionReplayEnrichmentPlugin { name = '@amplitude/plugin-session-replay-browser'; type = 'enrichment' as const; @@ -206,6 +206,8 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { }, packFn: pack, maskAllInputs: true, + maskTextClass: MASK_TEXT_CLASS, + maskInputFn, }); } diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index 84f959410..d0b5ed359 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -1,7 +1,6 @@ import { BrowserConfig, EnrichmentPlugin } from '@amplitude/analytics-types'; import { record } from 'rrweb'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SessionReplayOptions { sampleRate?: number; } diff --git a/packages/plugin-session-replay-browser/test/helpers.test.ts b/packages/plugin-session-replay-browser/test/helpers.test.ts new file mode 100644 index 000000000..e2283874e --- /dev/null +++ b/packages/plugin-session-replay-browser/test/helpers.test.ts @@ -0,0 +1,24 @@ +import { UNMASK_TEXT_CLASS } from '../src/constants'; +import { maskInputFn } from '../src/helpers'; + +describe('SessionReplayPlugin helpers', () => { + describe('maskInputFn', () => { + test('should not mask an element whose class list has amp-unmask in it', () => { + const htmlElement = document.createElement('div'); + htmlElement.classList.add(UNMASK_TEXT_CLASS); + const result = maskInputFn('some text', htmlElement); + expect(result).toEqual('some text'); + }); + test('should mask any other element', () => { + const htmlElement = document.createElement('div'); + htmlElement.classList.add('another-class'); + const result = maskInputFn('some text', htmlElement); + expect(result).toEqual('*********'); + }); + test('should handle an element without a class list', () => { + const htmlElement = {} as unknown as HTMLElement; + const result = maskInputFn('some text', htmlElement); + expect(result).toEqual('*********'); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 0a43256b5..b0e54f68f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,16 +2,75 @@ # yarn lockfile v1 -"@amplitude/analytics-connector@^1.4.5": +"@amplitude/analytics-browser@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-browser/-/analytics-browser-2.1.2.tgz#fadd74c56acfa163443db83b60ba2795294a6c31" + integrity sha512-1XoWJmLgGrszxupz61yCjen53bq7+kJN2gedu+kSrq/Ze9ow5R5QI0YfLyuzi1FHchzmSVWd2PheXOVYC4K/Cw== + dependencies: + "@amplitude/analytics-client-common" "^2.0.4" + "@amplitude/analytics-core" "^2.0.3" + "@amplitude/analytics-types" "^2.1.1" + "@amplitude/plugin-page-view-tracking-browser" "^2.0.4" + "@amplitude/plugin-web-attribution-browser" "^2.0.4" + tslib "^2.4.1" + +"@amplitude/analytics-client-common@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-client-common/-/analytics-client-common-2.0.4.tgz#59c9a86c68bdd53cc925afe17d295ab47bd9d7b3" + integrity sha512-X0+zE8sODcQ2ioj9ZB98Gr/9FCRDiJuSixefaLrfng/4x5VwHK0if8biCqqBHXu6HlMpeMFrCyiABUTDT87QVA== + dependencies: + "@amplitude/analytics-connector" "^1.4.8" + "@amplitude/analytics-core" "^2.0.3" + "@amplitude/analytics-types" "^2.1.1" + tslib "^2.4.1" + +"@amplitude/analytics-connector@^1.4.5", "@amplitude/analytics-connector@^1.4.8": version "1.4.8" resolved "https://registry.yarnpkg.com/@amplitude/analytics-connector/-/analytics-connector-1.4.8.tgz#dd801303db2662bc51be7e0194eeb8bd72267c42" integrity sha512-dFW7c7Wb6Ng7vbmzwbaXZSpqfBx37ukamJV9ErFYYS8vGZK/Hkbt3M7fZHBI4WFU6CCwakr2ZXPme11uGPYWkQ== +"@amplitude/analytics-core@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-core/-/analytics-core-2.0.3.tgz#491d29707765899abbe7da06d9d3a3677a8a9193" + integrity sha512-tpD1gCmPpzPNPumQT1ecOJtuan5OsQdKp9AX8YKc7t1/K3xHzGo3FH3JvdaAJVYYWeZV40bp/JL6wJiYIzyZjA== + dependencies: + "@amplitude/analytics-types" "^2.1.1" + tslib "^2.4.1" + +"@amplitude/analytics-types@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-types/-/analytics-types-2.1.1.tgz#89ef85397c4e6bc3a6aeaae4f3d4354ebb7de297" + integrity sha512-H3vebPR9onRdp0WzAZmI/4qmAE903uLOd2ZfMeHsVc1zaFTTCk46SoCuV4IrlF+VILrDw9Fy6gC9yl5N2PZcJQ== + +"@amplitude/plugin-page-view-tracking-browser@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.0.4.tgz#294c050f66810619816caa9027cfe6c61567e9b6" + integrity sha512-XQlgydCmfUb+mkXjD8NoOU80eO/578m2AupRcV0dPKwk+VN5D7D1s+xXYM6Bg7l1HSJx2mt0Qwa5xsdSCFzF2g== + dependencies: + "@amplitude/analytics-client-common" "^2.0.4" + "@amplitude/analytics-types" "^2.1.1" + tslib "^2.4.1" + +"@amplitude/plugin-web-attribution-browser@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@amplitude/plugin-web-attribution-browser/-/plugin-web-attribution-browser-2.0.4.tgz#30498aaff7c385cb14ebbb3976d6cc8df1794766" + integrity sha512-y/VQjDH7tnevRlKO9IWzELF9L87MwiR+y7WCRxR5/wRGKDxKS5Ol33vHYWe1RaOzkC/7wO0px7pZGRZkj/X+1Q== + dependencies: + "@amplitude/analytics-client-common" "^2.0.4" + "@amplitude/analytics-core" "^2.0.3" + "@amplitude/analytics-types" "^2.1.1" + tslib "^2.4.1" + "@amplitude/ua-parser-js@^0.7.31": version "0.7.31" resolved "https://registry.yarnpkg.com/@amplitude/ua-parser-js/-/ua-parser-js-0.7.31.tgz#749bf7cb633cfcc7ff3c10805bad7c5f6fbdbc61" integrity sha512-+z8UGRaj13Pt5NDzOnkTBy49HE2CX64jeL0ArB86HAtilpnfkPB7oqkigN7Lf2LxscMg4QhFD7mmCfedh3rqTg== +"@amplitude/ua-parser-js@^0.7.33": + version "0.7.33" + resolved "https://registry.yarnpkg.com/@amplitude/ua-parser-js/-/ua-parser-js-0.7.33.tgz#26441a0fb2e956a64e4ede50fb80b848179bb5db" + integrity sha512-wKEtVR4vXuPT9cVEIJkYWnlF++Gx3BdLatPBM+SZ1ztVIvnhdGBZR/mn9x/PzyrMcRlZmyi6L56I2J3doVBnjA== + "@ampproject/remapping@^2.1.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" @@ -3778,12 +3837,12 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@rrweb/types@^2.0.0-alpha.4": - version "2.0.0-alpha.8" - resolved "https://registry.yarnpkg.com/@rrweb/types/-/types-2.0.0-alpha.8.tgz#29a6550dd833364a459af4be4ce3b233003fb78b" - integrity sha512-yAr6ZQrgmr7+qZU5DMGqYXnVsolC5epftmZtkOtgFD/bbvCWflNnl09M32hUjttlKCV1ohhmQGioXkCQ37IF7A== +"@rrweb/types@^2.0.0-alpha.9": + version "2.0.0-alpha.9" + resolved "https://registry.yarnpkg.com/@rrweb/types/-/types-2.0.0-alpha.9.tgz#960cd224f7de7d0ba86852db0c04783320f92846" + integrity sha512-yS2KghLSmSSxo6H7tHrJ6u+nWJA9zCXaKFyc79rUSX8RHHSImRqocTqJ8jz794kCIWA90rvaQayRONdHO+vB0Q== dependencies: - rrweb-snapshot "^2.0.0-alpha.8" + rrweb-snapshot "^2.0.0-alpha.9" "@sideway/address@^4.1.3": version "4.1.4" @@ -11017,31 +11076,31 @@ rollup@^2.79.1: optionalDependencies: fsevents "~2.3.2" -rrdom@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/rrdom/-/rrdom-0.1.7.tgz#f2f49bfd01b59291bb7b0d981371a5e02a18e2aa" - integrity sha512-ZLd8f14z9pUy2Hk9y636cNv5Y2BMnNEY99wxzW9tD2BLDfe1xFxtLjB4q/xCBYo6HRe0wofzKzjm4JojmpBfFw== +rrdom@^2.0.0-alpha.9: + version "2.0.0-alpha.9" + resolved "https://registry.yarnpkg.com/rrdom/-/rrdom-2.0.0-alpha.9.tgz#a326d4176cec7eb8c9374ed8bb7da784252f09ef" + integrity sha512-jfaZ8tHi098P4GpPEtkOwnkucyKA5eGanAVHGPklzCqAeEq1Yx+9/y8AeOtF3yiobqKKkW8lLvFH2KrBH1CZlQ== dependencies: - rrweb-snapshot "^2.0.0-alpha.4" + rrweb-snapshot "^2.0.0-alpha.9" -rrweb-snapshot@^2.0.0-alpha.4, rrweb-snapshot@^2.0.0-alpha.8: - version "2.0.0-alpha.8" - resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.8.tgz#5f93f8ab7d78b3de26f6a64e993ff68edd54e0cf" - integrity sha512-3Rb7c+mnDEADQ8N9qn9SDH5PzCyHlZ1cwZC932qRyt9O8kJWLM11JLYqqEyQCa2FZVQbzH2iAaCgnyM7A32p7A== +rrweb-snapshot@^2.0.0-alpha.9: + version "2.0.0-alpha.9" + resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.9.tgz#4f504a4f266f0cb075ecbfa35aaf687d00483ca5" + integrity sha512-mHg1uUE2iUf0MXLE//4r5cMynkbduwmaOEis4gC7EuqkUAC1pYoLpcYYVt9lD6dgYIF6BmK6dgLLzMpD/tTyyA== -rrweb@^2.0.0-alpha.4: - version "2.0.0-alpha.4" - resolved "https://registry.yarnpkg.com/rrweb/-/rrweb-2.0.0-alpha.4.tgz#3c7cf2f1bcf44f7a88dd3fad00ee8d6dd711f258" - integrity sha512-wEHUILbxDPcNwkM3m4qgPgXAiBJyqCbbOHyVoNEVBJzHszWEFYyTbrZqUdeb1EfmTRC2PsumCIkVcomJ/xcOzA== +rrweb@^2.0.0-alpha.9: + version "2.0.0-alpha.9" + resolved "https://registry.yarnpkg.com/rrweb/-/rrweb-2.0.0-alpha.9.tgz#bce02ae1024251ece9d981bc7ebfe6a2eb6bbf9e" + integrity sha512-8E2yiLY7IrFjDcVUZ7AcQtdBNFuTIsBrlCMpbyLua6X64dGRhOZ+IUDXLnAbNj5oymZgFtZu2UERG9rmV2VAng== dependencies: - "@rrweb/types" "^2.0.0-alpha.4" + "@rrweb/types" "^2.0.0-alpha.9" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" fflate "^0.4.4" mitt "^3.0.0" - rrdom "^0.1.7" - rrweb-snapshot "^2.0.0-alpha.4" + rrdom "^2.0.0-alpha.9" + rrweb-snapshot "^2.0.0-alpha.9" run-async@^2.4.0: version "2.4.1" From 7587aac1c8f277a724aae09b8b1041923bee1d1d Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 27 Jul 2023 02:07:13 +0000 Subject: [PATCH 073/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.3.0 --- .../CHANGELOG.md | 27 +++++++++++++++++++ .../package.json | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 39e26d536..e1c4d4b45 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,33 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.3.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.2.1...@amplitude/plugin-session-replay-browser@0.3.0) (2023-07-27) + +### Bug Fixes + +- **plugins:** ensure config.optout takes priority over sample rate + ([5438a43](https://github.com/amplitude/Amplitude-TypeScript/commit/5438a438ec062b3011234b0fea82b54351a12864)) +- **plugins:** update README for session replay + ([ed20cf2](https://github.com/amplitude/Amplitude-TypeScript/commit/ed20cf22180b33f29fd71a1439de26706d6ec4ff)) + +### Features + +- **plugins:** abide by global opt out for session recording + ([25bd516](https://github.com/amplitude/Amplitude-TypeScript/commit/25bd516849c9a7fc346c6a868c664e70cdeb5cba)) +- **plugins:** add a configuration option for sampling rate + ([bcabfa4](https://github.com/amplitude/Amplitude-TypeScript/commit/bcabfa4ea784187edc85a85bce9a2c68dde411e5)) +- **plugins:** add default tracking of sessions to session replay plugin + ([6aeb511](https://github.com/amplitude/Amplitude-TypeScript/commit/6aeb511e5a7db760ebaa86aee0d7756fb85e9020)) +- **plugins:** add masking controls to session replay and reorganize constants and helper fns + ([e46a42e](https://github.com/amplitude/Amplitude-TypeScript/commit/e46a42e430c7560a4e9da322b10238fdbf4f1bc5)) +- **plugins:** update config additions to store in idb + ([5c04c3c](https://github.com/amplitude/Amplitude-TypeScript/commit/5c04c3cc0e8ef287898f2571f8c2a3e9e00311be)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.2.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.2.0...@amplitude/plugin-session-replay-browser@0.2.1) (2023-07-26) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 0d0902019..02212c532 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.2.1", + "version": "0.3.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From ed034358065d8f3dc6da4be9a880de5af001d6c2 Mon Sep 17 00:00:00 2001 From: Andrey Sokolov Date: Thu, 27 Jul 2023 22:00:02 +0400 Subject: [PATCH 074/214] fix: updated session logic and tests (#447) * fix: updated session logic and tests * fix: reverted session start logic in init * fix: fix appStateChange logic and tests * fix: do not update config.lastSessionTime for events with assigned session_id --- .../src/react-native-client.ts | 72 ++- .../test/react-native-client.test.ts | 43 +- .../test/react-native-sessions.test.ts | 608 ++++++++++++++++++ 3 files changed, 678 insertions(+), 45 deletions(-) create mode 100644 packages/analytics-react-native/test/react-native-sessions.test.ts diff --git a/packages/analytics-react-native/src/react-native-client.ts b/packages/analytics-react-native/src/react-native-client.ts index d10f72d31..5444fb705 100644 --- a/packages/analytics-react-native/src/react-native-client.ts +++ b/packages/analytics-react-native/src/react-native-client.ts @@ -84,7 +84,7 @@ export class AmplitudeReactNative extends AmplitudeCore { // Step 4: Manage session this.appState = AppState.currentState; - const isNewSession = this.startNewSessionIfNeeded(); + const isNewSession = this.startNewSessionIfNeeded(this.currentTimeMillis()); this.config.loggerProvider?.log( `Init: startNewSessionIfNeeded = ${isNewSession ? 'yes' : 'no'}, sessionId = ${ this.getSessionId() ?? 'undefined' @@ -230,19 +230,24 @@ export class AmplitudeReactNative extends AmplitudeCore { event = { ...event, time: eventTime }; } - if (event.event_type != START_SESSION_EVENT && event.event_type != END_SESSION_EVENT) { - if (this.appState !== 'active') { - this.startNewSessionIfNeeded(eventTime); + const isSessionEvent = event.event_type === START_SESSION_EVENT || event.event_type === END_SESSION_EVENT; + const isCustomEventSessionId = + !isSessionEvent && event.session_id != undefined && event.session_id !== this.getSessionId(); + if (!isCustomEventSessionId) { + if (!isSessionEvent) { + if (this.appState !== 'active') { + this.startNewSessionIfNeeded(eventTime); + } } + this.config.lastEventTime = eventTime; } - this.config.lastEventTime = eventTime; if (event.session_id == undefined) { event.session_id = this.getSessionId(); } if (event.event_id === undefined) { - const eventId = (this.config.lastEventId ?? -1) + 1; + const eventId = (this.config.lastEventId ?? 0) + 1; event = { ...event, event_id: eventId }; this.config.lastEventId = eventId; } @@ -255,25 +260,31 @@ export class AmplitudeReactNative extends AmplitudeCore { return Date.now(); } - private startNewSessionIfNeeded(eventTime?: number): boolean { - eventTime = eventTime ?? this.currentTimeMillis(); - const sessionId = this.explicitSessionId ?? eventTime; - - if ( - this.inSession() && - (this.explicitSessionId === this.config.sessionId || - (this.explicitSessionId === undefined && this.isWithinMinTimeBetweenSessions(sessionId))) - ) { - this.config.lastEventTime = eventTime; - return false; + private startNewSessionIfNeeded(timestamp: number): boolean { + const sessionId = this.explicitSessionId ?? timestamp; + + const shouldStartNewSession = this.shouldStartNewSession(timestamp); + if (shouldStartNewSession) { + this.setSessionIdInternal(sessionId, timestamp); + } else { + this.config.lastEventTime = timestamp; } - this.setSessionIdInternal(sessionId, eventTime); - return true; + return shouldStartNewSession; + } + + private shouldStartNewSession(timestamp: number): boolean { + const sessionId = this.explicitSessionId ?? timestamp; + + return ( + !this.inSession() || + (this.explicitSessionId !== this.config.sessionId && + (this.explicitSessionId !== undefined || !this.isWithinMinTimeBetweenSessions(sessionId))) + ); } - private isWithinMinTimeBetweenSessions(eventTime: number) { - return eventTime - (this.config.lastEventTime ?? 0) < this.config.sessionTimeout; + private isWithinMinTimeBetweenSessions(timestamp: number) { + return timestamp - (this.config.lastEventTime ?? 0) < this.config.sessionTimeout; } private inSession() { @@ -283,11 +294,24 @@ export class AmplitudeReactNative extends AmplitudeCore { private readonly handleAppStateChange = (nextAppState: AppStateStatus) => { const currentAppState = this.appState; this.appState = nextAppState; - if (currentAppState !== nextAppState && nextAppState === 'active') { - this.config.loggerProvider?.log('App Activated'); - this.startNewSessionIfNeeded(); + if (currentAppState !== nextAppState) { + const timestamp = this.currentTimeMillis(); + if (nextAppState == 'active') { + this.enterForeground(timestamp); + } else { + this.exitForeground(timestamp); + } } }; + + private enterForeground(timestamp: number) { + this.config.loggerProvider?.log('App Activated'); + return this.startNewSessionIfNeeded(timestamp); + } + + private exitForeground(timestamp: number) { + this.config.lastEventTime = timestamp; + } } export const createInstance = (): ReactNativeClient => { diff --git a/packages/analytics-react-native/test/react-native-client.test.ts b/packages/analytics-react-native/test/react-native-client.test.ts index dde1d017b..8feb641c6 100644 --- a/packages/analytics-react-native/test/react-native-client.test.ts +++ b/packages/analytics-react-native/test/react-native-client.test.ts @@ -462,7 +462,8 @@ describe('react-native-client', () => { this.handleAppStateChange('active'); } - setBackground() { + setBackground(timestamp: number) { + this.currentTime = timestamp; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.handleAppStateChange('background'); @@ -500,46 +501,46 @@ describe('react-native-client', () => { const send = jest.fn().mockReturnValue(sendResponse); const cookieStorage = new core.MemoryStorage(); - const client1 = new AmplitudeReactNativeTest(500); + const client1 = new AmplitudeReactNativeTest(950); await client1.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; client1.setActive(1000); - expect(client1.config.sessionId).toEqual(1000); + expect(client1.config.sessionId).toEqual(950); expect(client1.config.lastEventTime).toEqual(1000); - expect(client1.config.lastEventId).toEqual(2); + expect(client1.config.lastEventId).toEqual(1); void client1.track({ event_type: 'event-1', time: 1200 }); - expect(client1.config.sessionId).toEqual(1000); + expect(client1.config.sessionId).toEqual(950); expect(client1.config.lastEventTime).toEqual(1200); - expect(client1.config.lastEventId).toEqual(3); + expect(client1.config.lastEventId).toEqual(2); const client2 = new AmplitudeReactNativeTest(1250); await client2.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; - expect(client2.config.sessionId).toEqual(1000); + expect(client2.config.sessionId).toEqual(950); expect(client2.config.lastEventTime).toEqual(1250); - expect(client2.config.lastEventId).toEqual(3); + expect(client2.config.lastEventId).toEqual(2); void client2.track({ event_type: 'event-2', time: 1270 }); - expect(client2.config.sessionId).toEqual(1000); + expect(client2.config.sessionId).toEqual(950); expect(client2.config.lastEventTime).toEqual(1270); - expect(client2.config.lastEventId).toEqual(4); + expect(client2.config.lastEventId).toEqual(3); const client3 = new AmplitudeReactNativeTest(1300); await client3.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; - expect(client3.config.sessionId).toEqual(1000); + expect(client3.config.sessionId).toEqual(950); expect(client3.config.lastEventTime).toEqual(1300); - expect(client3.config.lastEventId).toEqual(4); + expect(client3.config.lastEventId).toEqual(3); client3.setActive(1500); expect(client3.config.sessionId).toEqual(1500); expect(client3.config.lastEventTime).toEqual(1500); - expect(client3.config.lastEventId).toEqual(6); + expect(client3.config.lastEventId).toEqual(5); }); describe('track session events', () => { @@ -560,7 +561,7 @@ describe('react-native-client', () => { void client.track({ event_type: 'event-5', time: 1700 }); - client.setBackground(); + client.setBackground(1730); void client.track({ event_type: 'event-6', time: 1750 }); void client.track({ event_type: 'event-7', time: 2000 }); @@ -572,7 +573,7 @@ describe('react-native-client', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); expect(events.length).toEqual(18); - events.forEach((event, i) => expect(event.event_id).toEqual(i)); + events.forEach((event, i) => expect(event.event_id).toEqual(i + 1)); expect(events[0].event_type).toEqual('session_end'); expect(events[0].session_id).toEqual(500); @@ -671,7 +672,7 @@ describe('react-native-client', () => { client.currentTime = 1720; client.setSessionId(5050); - client.setBackground(); + client.setBackground(1730); void client.track({ event_type: 'event-6', time: 1750 }); void client.track({ event_type: 'event-7', time: 2000 }); @@ -685,7 +686,7 @@ describe('react-native-client', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); expect(events.length).toEqual(14); - events.forEach((event, i) => expect(event.event_id).toEqual(i)); + events.forEach((event, i) => expect(event.event_id).toEqual(i + 1)); expect(events[0].event_type).toEqual('session_end'); expect(events[0].session_id).toEqual(500); @@ -763,7 +764,7 @@ describe('react-native-client', () => { void client.track({ event_type: 'event-5', time: 1700 }); - client.setBackground(); + client.setBackground(1730); void client.track({ event_type: 'event-6', time: 1750 }); void client.track({ event_type: 'event-7', time: 2000 }); @@ -775,7 +776,7 @@ describe('react-native-client', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); expect(events.length).toEqual(8); - events.forEach((event, i) => expect(event.event_id).toEqual(i)); + events.forEach((event, i) => expect(event.event_id).toEqual(i + 1)); expect(events[0].event_type).toEqual('event-1'); expect(events[0].session_id).toEqual(950); @@ -834,7 +835,7 @@ describe('react-native-client', () => { client.currentTime = 1720; client.setSessionId(5050); - client.setBackground(); + client.setBackground(1730); void client.track({ event_type: 'event-6', time: 1750 }); void client.track({ event_type: 'event-7', time: 2000 }); @@ -848,7 +849,7 @@ describe('react-native-client', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); expect(events.length).toEqual(8); - events.forEach((event, i) => expect(event.event_id).toEqual(i)); + events.forEach((event, i) => expect(event.event_id).toEqual(i + 1)); expect(events[0].event_type).toEqual('event-1'); expect(events[0].session_id).toEqual(1000); diff --git a/packages/analytics-react-native/test/react-native-sessions.test.ts b/packages/analytics-react-native/test/react-native-sessions.test.ts new file mode 100644 index 000000000..aaed17526 --- /dev/null +++ b/packages/analytics-react-native/test/react-native-sessions.test.ts @@ -0,0 +1,608 @@ +import { AmplitudeReactNative } from '../src/react-native-client'; +import * as core from '@amplitude/analytics-core'; +import { Status, UserSession, Event } from '@amplitude/analytics-types'; +import { isWeb } from '../src/utils/platform'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +describe('react-native-session', () => { + const API_KEY = 'API_KEY'; + const attributionConfig = { + attribution: { + disabled: true, + }, + }; + + afterEach(async () => { + // clean up cookies + // due to jest env, cookies are always preset and needs to be cleaned up + document.cookie = 'AMP_API_KEY=null; expires=-1'; + if (!isWeb()) { + await AsyncStorage.clear(); + } + }); + + class AmplitudeReactNativeTest extends AmplitudeReactNative { + currentTime: number; + + constructor(currentTime: number) { + super(); + this.currentTime = currentTime; + } + + currentTimeMillis(): number { + return this.currentTime; + } + + setForeground(timestamp: number) { + this.currentTime = timestamp; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.handleAppStateChange('active'); + } + + setBackground(timestamp: number) { + this.currentTime = timestamp; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.handleAppStateChange('background'); + } + } + + const sendResponse = { + status: Status.Success, + statusCode: 200, + body: { + eventsIngested: 1, + payloadSizeBytes: 1, + serverUploadTime: 1, + }, + }; + + const clientOptions = ( + send: any, + cookieStorage: core.MemoryStorage, + trackingSessionEvents: boolean, + sessionId?: number, + ) => ({ + transportProvider: { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + send, + }, + cookieStorage, + sessionTimeout: 100, + sessionId, + trackingSessionEvents, + ...attributionConfig, + }); + + const createEvent = (time: number, eventType: string, sessionId?: number): Event => { + return { + event_type: eventType, + time: time, + session_id: sessionId, + user_id: 'user', + }; + }; + + test('close background events should not start new session', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client = new AmplitudeReactNativeTest(950); + await client.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + client.track(createEvent(1000, 'event-1')); + client.track(createEvent(1050, 'event-2')); + await client.flush().promise; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); + expect(events.length).toEqual(3); + + expect(events[0].event_type).toEqual('session_start'); + expect(events[0].session_id).toEqual(950); + expect(events[0].time).toEqual(950); + + expect(events[1].event_type).toEqual('event-1'); + expect(events[1].session_id).toEqual(950); + expect(events[1].time).toEqual(1000); + + expect(events[2].event_type).toEqual('event-2'); + expect(events[2].session_id).toEqual(950); + expect(events[2].time).toEqual(1050); + }); + + test('distant background events should start new session', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client = new AmplitudeReactNativeTest(950); + await client.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + client.track(createEvent(1000, 'event-1')); + client.track(createEvent(2000, 'event-2')); + await client.flush().promise; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); + expect(events.length).toEqual(5); + + expect(events[0].event_type).toEqual('session_start'); + expect(events[0].session_id).toEqual(950); + expect(events[0].time).toEqual(950); + + expect(events[1].event_type).toEqual('event-1'); + expect(events[1].session_id).toEqual(950); + expect(events[1].time).toEqual(1000); + + expect(events[2].event_type).toEqual('session_end'); + expect(events[2].session_id).toEqual(950); + expect(events[2].time).toEqual(1001); + + expect(events[3].event_type).toEqual('session_start'); + expect(events[3].session_id).toEqual(2000); + expect(events[3].time).toEqual(2000); + + expect(events[4].event_type).toEqual('event-2'); + expect(events[4].session_id).toEqual(2000); + expect(events[4].time).toEqual(2000); + }); + + test('foreground events should not start new session', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client = new AmplitudeReactNativeTest(950); + await client.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + client.setForeground(1000); + client.track(createEvent(1050, 'event-1')); + client.track(createEvent(2000, 'event-2')); + await client.flush().promise; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); + expect(events.length).toEqual(3); + + expect(events[0].event_type).toEqual('session_start'); + expect(events[0].session_id).toEqual(950); + expect(events[0].time).toEqual(950); + + expect(events[1].event_type).toEqual('event-1'); + expect(events[1].session_id).toEqual(950); + expect(events[1].time).toEqual(1050); + + expect(events[2].event_type).toEqual('event-2'); + expect(events[2].session_id).toEqual(950); + expect(events[2].time).toEqual(2000); + }); + + test('close background and foreground events should not start new session', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client = new AmplitudeReactNativeTest(950); + await client.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + client.track(createEvent(1000, 'event-1')); + client.setForeground(1050); + client.track(createEvent(2000, 'event-2')); + await client.flush().promise; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); + expect(events.length).toEqual(3); + + expect(events[0].event_type).toEqual('session_start'); + expect(events[0].session_id).toEqual(950); + expect(events[0].time).toEqual(950); + + expect(events[1].event_type).toEqual('event-1'); + expect(events[1].session_id).toEqual(950); + expect(events[1].time).toEqual(1000); + + expect(events[2].event_type).toEqual('event-2'); + expect(events[2].session_id).toEqual(950); + expect(events[2].time).toEqual(2000); + }); + + test('distant background and foreground events should start new session', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client = new AmplitudeReactNativeTest(950); + await client.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + client.track(createEvent(1000, 'event-1')); + client.setForeground(2000); + client.track(createEvent(3000, 'event-2')); + await client.flush().promise; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); + expect(events.length).toEqual(5); + + expect(events[0].event_type).toEqual('session_start'); + expect(events[0].session_id).toEqual(950); + expect(events[0].time).toEqual(950); + + expect(events[1].event_type).toEqual('event-1'); + expect(events[1].session_id).toEqual(950); + expect(events[1].time).toEqual(1000); + + expect(events[2].event_type).toEqual('session_end'); + expect(events[2].session_id).toEqual(950); + expect(events[2].time).toEqual(1001); + + expect(events[3].event_type).toEqual('session_start'); + expect(events[3].session_id).toEqual(2000); + expect(events[3].time).toEqual(2000); + + expect(events[4].event_type).toEqual('event-2'); + expect(events[4].session_id).toEqual(2000); + expect(events[4].time).toEqual(3000); + }); + + test('close foreground and background events should not start new session', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client = new AmplitudeReactNativeTest(950); + await client.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + client.setForeground(1000); + client.track(createEvent(1500, 'event-1')); + client.setBackground(2000); + client.track(createEvent(2050, 'event-2')); + await client.flush().promise; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); + expect(events.length).toEqual(3); + + expect(events[0].event_type).toEqual('session_start'); + expect(events[0].session_id).toEqual(950); + expect(events[0].time).toEqual(950); + + expect(events[1].event_type).toEqual('event-1'); + expect(events[1].session_id).toEqual(950); + expect(events[1].time).toEqual(1500); + + expect(events[2].event_type).toEqual('event-2'); + expect(events[2].session_id).toEqual(950); + expect(events[2].time).toEqual(2050); + }); + + test('distant foreground and background events should start new session', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client = new AmplitudeReactNativeTest(950); + await client.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + client.setForeground(1000); + client.track(createEvent(1500, 'event-1')); + client.setBackground(2000); + client.track(createEvent(3000, 'event-2')); + await client.flush().promise; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); + expect(events.length).toEqual(5); + + expect(events[0].event_type).toEqual('session_start'); + expect(events[0].session_id).toEqual(950); + expect(events[0].time).toEqual(950); + + expect(events[1].event_type).toEqual('event-1'); + expect(events[1].session_id).toEqual(950); + expect(events[1].time).toEqual(1500); + + expect(events[2].event_type).toEqual('session_end'); + expect(events[2].session_id).toEqual(950); + expect(events[2].time).toEqual(2001); + + expect(events[3].event_type).toEqual('session_start'); + expect(events[3].session_id).toEqual(3000); + expect(events[3].time).toEqual(3000); + + expect(events[4].event_type).toEqual('event-2'); + expect(events[4].session_id).toEqual(3000); + expect(events[4].time).toEqual(3000); + }); + + test('session data should be persisted', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client1 = new AmplitudeReactNativeTest(950); + await client1.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + client1.setForeground(1000); + + expect(client1.getSessionId()).toEqual(950); + expect(client1.config.sessionId).toEqual(950); + expect(client1.config.lastEventTime).toEqual(1000); + expect(client1.config.lastEventId).toEqual(1); + + client1.track(createEvent(1200, 'event-1')); + + expect(client1.getSessionId()).toEqual(950); + expect(client1.config.sessionId).toEqual(950); + expect(client1.config.lastEventTime).toEqual(1200); + expect(client1.config.lastEventId).toEqual(2); + + const client2 = new AmplitudeReactNativeTest(1250); + await client2.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + expect(client2.getSessionId()).toEqual(950); + expect(client2.config.sessionId).toEqual(950); + expect(client2.config.lastEventTime).toEqual(1250); + expect(client1.config.lastEventId).toEqual(2); + }); + + test('explicit session for event should be preserved and do not update config.lastEventTime', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client = new AmplitudeReactNativeTest(950); + await client.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + expect(client.config.lastEventTime).toEqual(950); + client.track(createEvent(1000, 'event-1')); + expect(client.config.lastEventTime).toEqual(1000); + client.track(createEvent(1050, 'event-2', 3000)); + expect(client.config.lastEventTime).toEqual(1000); + client.track(createEvent(1100, 'event-3')); + expect(client.config.lastEventTime).toEqual(1100); + await client.flush().promise; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); + expect(events.length).toEqual(6); + + expect(events[0].event_type).toEqual('session_start'); + expect(events[0].session_id).toEqual(950); + expect(events[0].time).toEqual(950); + + expect(events[1].event_type).toEqual('event-1'); + expect(events[1].session_id).toEqual(950); + expect(events[1].time).toEqual(1000); + + expect(events[2].event_type).toEqual('event-2'); + expect(events[2].session_id).toEqual(3000); + expect(events[2].time).toEqual(1050); + + expect(events[3].event_type).toEqual('session_end'); + expect(events[3].session_id).toEqual(950); + expect(events[3].time).toEqual(1001); + + expect(events[4].event_type).toEqual('session_start'); + expect(events[4].session_id).toEqual(1100); + expect(events[4].time).toEqual(1100); + + expect(events[5].event_type).toEqual('event-3'); + expect(events[5].session_id).toEqual(1100); + expect(events[5].time).toEqual(1100); + }); + + test('explicit no session for event should be preserved and do not update config.lastEventTime', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client = new AmplitudeReactNativeTest(950); + await client.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + expect(client.config.lastEventTime).toEqual(950); + client.track(createEvent(1000, 'event-1')); + expect(client.config.lastEventTime).toEqual(1000); + client.track(createEvent(1050, 'event-2', -1)); + expect(client.config.lastEventTime).toEqual(1000); + client.track(createEvent(1100, 'event-3')); + expect(client.config.lastEventTime).toEqual(1100); + await client.flush().promise; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); + expect(events.length).toEqual(6); + + expect(events[0].event_type).toEqual('session_start'); + expect(events[0].session_id).toEqual(950); + expect(events[0].time).toEqual(950); + + expect(events[1].event_type).toEqual('event-1'); + expect(events[1].session_id).toEqual(950); + expect(events[1].time).toEqual(1000); + + expect(events[2].event_type).toEqual('event-2'); + expect(events[2].session_id).toEqual(-1); + expect(events[2].time).toEqual(1050); + + expect(events[3].event_type).toEqual('session_end'); + expect(events[3].session_id).toEqual(950); + expect(events[3].time).toEqual(1001); + + expect(events[4].event_type).toEqual('session_start'); + expect(events[4].session_id).toEqual(1100); + expect(events[4].time).toEqual(1100); + + expect(events[5].event_type).toEqual('event-3'); + expect(events[5].session_id).toEqual(1100); + expect(events[5].time).toEqual(1100); + }); + + test('explicit session for event (equal to current session) should be preserved and update config.lastEventTime', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client = new AmplitudeReactNativeTest(950); + await client.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + expect(client.config.lastEventTime).toEqual(950); + client.track(createEvent(1000, 'event-1')); + expect(client.config.lastEventTime).toEqual(1000); + client.track(createEvent(1050, 'event-2', 950)); + expect(client.config.lastEventTime).toEqual(1050); + client.track(createEvent(1100, 'event-3')); + expect(client.config.lastEventTime).toEqual(1100); + await client.flush().promise; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); + expect(events.length).toEqual(4); + + expect(events[0].event_type).toEqual('session_start'); + expect(events[0].session_id).toEqual(950); + expect(events[0].time).toEqual(950); + + expect(events[1].event_type).toEqual('event-1'); + expect(events[1].session_id).toEqual(950); + expect(events[1].time).toEqual(1000); + + expect(events[2].event_type).toEqual('event-2'); + expect(events[2].session_id).toEqual(950); + expect(events[2].time).toEqual(1050); + + expect(events[3].event_type).toEqual('event-3'); + expect(events[3].session_id).toEqual(950); + expect(events[3].time).toEqual(1100); + }); + + describe('explicit global session', () => { + test('explicit session should be used', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client = new AmplitudeReactNativeTest(5000); + await client.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + client.setSessionId(5000); + client.track(createEvent(1000, 'event-1')); + client.track(createEvent(1050, 'event-2')); + client.currentTime = 1070; + client.setSessionId(6000); + client.track(createEvent(1100, 'event-3')); + await client.flush().promise; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); + expect(events.length).toEqual(6); + + expect(events[0].event_type).toEqual('session_start'); + expect(events[0].session_id).toEqual(5000); + expect(events[0].time).toEqual(5000); + + expect(events[1].event_type).toEqual('event-1'); + expect(events[1].session_id).toEqual(5000); + expect(events[1].time).toEqual(1000); + + expect(events[2].event_type).toEqual('event-2'); + expect(events[2].session_id).toEqual(5000); + expect(events[2].time).toEqual(1050); + + expect(events[3].event_type).toEqual('session_end'); + expect(events[3].session_id).toEqual(5000); + expect(events[3].time).toEqual(1051); + + expect(events[4].event_type).toEqual('session_start'); + expect(events[4].session_id).toEqual(6000); + expect(events[4].time).toEqual(1070); + + expect(events[5].event_type).toEqual('event-3'); + expect(events[5].session_id).toEqual(6000); + expect(events[5].time).toEqual(1100); + }); + + test('explicit session for event should be preserved', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client = new AmplitudeReactNativeTest(950); + await client.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + client.setSessionId(5000); + expect(client.config.lastEventTime).toEqual(950); + client.track(createEvent(1000, 'event-1')); + expect(client.config.lastEventTime).toEqual(1000); + client.track(createEvent(1050, 'event-2', 3000)); + expect(client.config.lastEventTime).toEqual(1000); + client.track(createEvent(1100, 'event-3')); + expect(client.config.lastEventTime).toEqual(1100); + await client.flush().promise; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); + expect(events.length).toEqual(6); + + expect(events[0].event_type).toEqual('session_start'); + expect(events[0].session_id).toEqual(950); + expect(events[0].time).toEqual(950); + + expect(events[1].event_type).toEqual('session_end'); + expect(events[1].session_id).toEqual(950); + expect(events[1].time).toEqual(951); + + expect(events[2].event_type).toEqual('session_start'); + expect(events[2].session_id).toEqual(5000); + expect(events[2].time).toEqual(950); + + expect(events[3].event_type).toEqual('event-1'); + expect(events[3].session_id).toEqual(5000); + expect(events[3].time).toEqual(1000); + + expect(events[4].event_type).toEqual('event-2'); + expect(events[4].session_id).toEqual(3000); + expect(events[4].time).toEqual(1050); + + expect(events[5].event_type).toEqual('event-3'); + expect(events[5].session_id).toEqual(5000); + expect(events[5].time).toEqual(1100); + }); + + test('explicit no session for event should be preserved', async () => { + const send = jest.fn().mockReturnValue(sendResponse); + const cookieStorage = new core.MemoryStorage(); + + const client = new AmplitudeReactNativeTest(950); + await client.init(API_KEY, undefined, clientOptions(send, cookieStorage, true)).promise; + + client.setSessionId(5000); + expect(client.config.lastEventTime).toEqual(950); + client.track(createEvent(1000, 'event-1')); + expect(client.config.lastEventTime).toEqual(1000); + client.track(createEvent(1050, 'event-2', -1)); + expect(client.config.lastEventTime).toEqual(1000); + client.track(createEvent(1100, 'event-3')); + expect(client.config.lastEventTime).toEqual(1100); + await client.flush().promise; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const events = send.mock.calls.flatMap((call) => call[1].events as Event[]); + expect(events.length).toEqual(6); + + expect(events[0].event_type).toEqual('session_start'); + expect(events[0].session_id).toEqual(950); + expect(events[0].time).toEqual(950); + + expect(events[1].event_type).toEqual('session_end'); + expect(events[1].session_id).toEqual(950); + expect(events[1].time).toEqual(951); + + expect(events[2].event_type).toEqual('session_start'); + expect(events[2].session_id).toEqual(5000); + expect(events[2].time).toEqual(950); + + expect(events[3].event_type).toEqual('event-1'); + expect(events[3].session_id).toEqual(5000); + expect(events[3].time).toEqual(1000); + + expect(events[4].event_type).toEqual('event-2'); + expect(events[4].session_id).toEqual(-1); + expect(events[4].time).toEqual(1050); + + expect(events[5].event_type).toEqual('event-3'); + expect(events[5].session_id).toEqual(5000); + expect(events[5].time).toEqual(1100); + }); + }); +}); From fb261b89db96e707de6509ccbf57f319c696ef27 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 27 Jul 2023 13:14:47 -0700 Subject: [PATCH 075/214] feat(plugins): add option to block elements based on amp-block class in session replay --- packages/plugin-session-replay-browser/README.md | 2 ++ packages/plugin-session-replay-browser/src/constants.ts | 1 + packages/plugin-session-replay-browser/src/session-replay.ts | 2 ++ 3 files changed, 5 insertions(+) diff --git a/packages/plugin-session-replay-browser/README.md b/packages/plugin-session-replay-browser/README.md index 66dd0b95d..cf1a789c3 100644 --- a/packages/plugin-session-replay-browser/README.md +++ b/packages/plugin-session-replay-browser/README.md @@ -70,3 +70,5 @@ In your application code, add the class `.amp-unmask` to any __input__ whose tex #### 2. Mask non-input elements In your application code, add the class `.amp-mask` to any __non-input element__ whose text you'd like to have masked from the recording. The text in the element, as well as it's children, will all be converted to asterisks. +#### 3. Block non-text elements +In your application code, add the class `.amp-block` to any element you would like to have blocked from the recording. The element will appear in the replay as a placeholder with the same dimensions. diff --git a/packages/plugin-session-replay-browser/src/constants.ts b/packages/plugin-session-replay-browser/src/constants.ts index d726dbc10..55936ce9e 100644 --- a/packages/plugin-session-replay-browser/src/constants.ts +++ b/packages/plugin-session-replay-browser/src/constants.ts @@ -7,6 +7,7 @@ export const DEFAULT_SESSION_REPLAY_PROPERTY = `${DEFAULT_EVENT_PROPERTY_PREFIX} export const DEFAULT_SESSION_START_EVENT = 'session_start'; export const DEFAULT_SESSION_END_EVENT = 'session_end'; +export const BLOCK_CLASS = 'amp-block'; export const MASK_TEXT_CLASS = 'amp-mask'; export const UNMASK_TEXT_CLASS = 'amp-unmask'; export const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/track'; diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 31b62be68..f25573665 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -4,6 +4,7 @@ import { BrowserConfig, Event, Status } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import { pack, record } from 'rrweb'; import { + BLOCK_CLASS, DEFAULT_SESSION_END_EVENT, DEFAULT_SESSION_REPLAY_PROPERTY, DEFAULT_SESSION_START_EVENT, @@ -207,6 +208,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { packFn: pack, maskAllInputs: true, maskTextClass: MASK_TEXT_CLASS, + blockClass: BLOCK_CLASS, maskInputFn, }); } From 66d8395c7bac0a60252a80ddf03e037301150b86 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 27 Jul 2023 21:09:55 +0000 Subject: [PATCH 076/214] chore(release): publish - @amplitude/analytics-react-native@1.3.3 - @amplitude/plugin-session-replay-browser@0.4.0 --- packages/analytics-react-native/CHANGELOG.md | 12 ++++++++++++ packages/analytics-react-native/package.json | 2 +- packages/analytics-react-native/src/version.ts | 2 +- packages/plugin-session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/plugin-session-replay-browser/package.json | 2 +- 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/analytics-react-native/CHANGELOG.md b/packages/analytics-react-native/CHANGELOG.md index 5159e4202..1110778e2 100644 --- a/packages/analytics-react-native/CHANGELOG.md +++ b/packages/analytics-react-native/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.3.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-react-native@1.3.2...@amplitude/analytics-react-native@1.3.3) (2023-07-27) + +### Bug Fixes + +- updated session logic and tests ([#447](https://github.com/amplitude/Amplitude-TypeScript/issues/447)) + ([ed03435](https://github.com/amplitude/Amplitude-TypeScript/commit/ed034358065d8f3dc6da4be9a880de5af001d6c2)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.3.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-react-native@1.3.1...@amplitude/analytics-react-native@1.3.2) (2023-07-26) ### Bug Fixes diff --git a/packages/analytics-react-native/package.json b/packages/analytics-react-native/package.json index b8d85189a..d48e80130 100644 --- a/packages/analytics-react-native/package.json +++ b/packages/analytics-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-react-native", - "version": "1.3.2", + "version": "1.3.3", "description": "Official React Native SDK", "keywords": [ "analytics", diff --git a/packages/analytics-react-native/src/version.ts b/packages/analytics-react-native/src/version.ts index 45f295c35..181c51b5a 100644 --- a/packages/analytics-react-native/src/version.ts +++ b/packages/analytics-react-native/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.3.2'; +export const VERSION = '1.3.3'; diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index e1c4d4b45..098898744 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.4.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.3.0...@amplitude/plugin-session-replay-browser@0.4.0) (2023-07-27) + +### Features + +- **plugins:** add option to block elements based on amp-block class in session replay + ([fb261b8](https://github.com/amplitude/Amplitude-TypeScript/commit/fb261b89db96e707de6509ccbf57f319c696ef27)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [0.3.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.2.1...@amplitude/plugin-session-replay-browser@0.3.0) (2023-07-27) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 02212c532..36645d533 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.3.0", + "version": "0.4.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 675cb82d248f5eb564150be479228469f12d4fc8 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 27 Jul 2023 15:48:13 -0700 Subject: [PATCH 077/214] fix(plugins): prevent any activity from occurring if document does not have focus --- .../src/session-replay.ts | 11 +++++-- .../test/session-replay.test.ts | 29 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index f25573665..f2509105a 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -75,8 +75,6 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { }; } - await this.initialize(true); - const GlobalScope = getGlobalScope(); if (GlobalScope && GlobalScope.window) { GlobalScope.window.addEventListener('blur', () => { @@ -87,9 +85,18 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { void this.initialize(); }); } + + if (GlobalScope && GlobalScope.document && GlobalScope.document.hasFocus()) { + await this.initialize(true); + } } async execute(event: Event) { + const GlobalScope = getGlobalScope(); + if (GlobalScope && GlobalScope.document && !GlobalScope.document.hasFocus()) { + return Promise.resolve(event); + } + if (this.shouldRecord) { event.event_properties = { ...event.event_properties, diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 3c9615ef1..27840a4aa 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -151,6 +151,16 @@ describe('SessionReplayPlugin', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(initialize.mock.calls[0]).toEqual([]); }); + test('it should not call initialize if the document does not have focus', () => { + const sessionReplay = sessionReplayPlugin(); + const initialize = jest.spyOn(sessionReplay, 'initialize').mockReturnValueOnce(Promise.resolve()); + jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue({ + document: { + hasFocus: () => false, + }, + } as typeof globalThis); + expect(initialize).not.toHaveBeenCalled(); + }); }); describe('initalize', () => { @@ -410,6 +420,25 @@ describe('SessionReplayPlugin', () => { }); describe('execute', () => { + test('it should return event if document does not have focus', async () => { + const sessionReplay = sessionReplayPlugin(); + jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue({ + document: { + hasFocus: () => false, + }, + } as typeof globalThis); + await sessionReplay.setup(mockConfig); + const event = { + event_type: 'event_type', + event_properties: { + property_a: true, + property_b: 123, + }, + }; + + const executedEvent = await sessionReplay.execute(event); + expect(executedEvent).toEqual(event); + }); test('should add event property for [Amplitude] Session Recorded', async () => { const sessionReplay = sessionReplayPlugin(); await sessionReplay.setup(mockConfig); From 856f91dc11c7f0336a800e299838ef2e2b8556df Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 27 Jul 2023 23:15:02 +0000 Subject: [PATCH 078/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.4.1 --- packages/plugin-session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/plugin-session-replay-browser/package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 098898744..086db4822 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.4.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.4.0...@amplitude/plugin-session-replay-browser@0.4.1) (2023-07-27) + +### Bug Fixes + +- **plugins:** prevent any activity from occurring if document does not have focus + ([675cb82](https://github.com/amplitude/Amplitude-TypeScript/commit/675cb82d248f5eb564150be479228469f12d4fc8)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [0.4.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.3.0...@amplitude/plugin-session-replay-browser@0.4.0) (2023-07-27) ### Features diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 36645d533..5874170a3 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.4.0", + "version": "0.4.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 2d4362f845148e7f45411e1e7db8482cd71366ab Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Sun, 30 Jul 2023 23:11:30 -0700 Subject: [PATCH 079/214] chore: plugin-user-agent-enrichment-browser@1.0.0-beta.0 (#513) --- packages/plugin-user-agent-enrichment-browser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-user-agent-enrichment-browser/package.json b/packages/plugin-user-agent-enrichment-browser/package.json index a801a263a..9367f675a 100644 --- a/packages/plugin-user-agent-enrichment-browser/package.json +++ b/packages/plugin-user-agent-enrichment-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-user-agent-enrichment-browser", - "version": "0.1.0", + "version": "1.0.0-beta.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From d7c22fca2eda201f427d6a730abb5e82fb7e66dc Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Mon, 31 Jul 2023 06:31:05 +0000 Subject: [PATCH 080/214] chore(release): publish - @amplitude/plugin-user-agent-enrichment-browser@1.0.0-beta.1 --- .../plugin-user-agent-enrichment-browser/CHANGELOG.md | 9 +++++++++ .../plugin-user-agent-enrichment-browser/package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md b/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md index 649bb0b8f..87379728b 100644 --- a/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md +++ b/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.0.0-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-user-agent-enrichment-browser@0.1.0...@amplitude/plugin-user-agent-enrichment-browser@1.0.0-beta.1) (2023-07-31) + +**Note:** Version bump only for package @amplitude/plugin-user-agent-enrichment-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # 0.1.0 (2023-07-26) ### Features diff --git a/packages/plugin-user-agent-enrichment-browser/package.json b/packages/plugin-user-agent-enrichment-browser/package.json index 9367f675a..5cd62c93b 100644 --- a/packages/plugin-user-agent-enrichment-browser/package.json +++ b/packages/plugin-user-agent-enrichment-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-user-agent-enrichment-browser", - "version": "1.0.0-beta.0", + "version": "1.0.0-beta.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 1e8317b64d8868bd4b5e584f50ffec4ca54dcadc Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Mon, 31 Jul 2023 13:25:33 -0400 Subject: [PATCH 081/214] fix(plugins): correctly fetch shouldRecord from store and clean up old store entries --- .../src/constants.ts | 1 + .../src/session-replay.ts | 32 +++--- .../test/session-replay.test.ts | 100 +++++++++++------- 3 files changed, 79 insertions(+), 54 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/constants.ts b/packages/plugin-session-replay-browser/src/constants.ts index 55936ce9e..77fda5a2e 100644 --- a/packages/plugin-session-replay-browser/src/constants.ts +++ b/packages/plugin-session-replay-browser/src/constants.ts @@ -21,3 +21,4 @@ export const defaultSessionStore: IDBStoreSession = { currentSequenceId: 0, sessionSequences: {}, }; +export const MAX_IDB_STORAGE_LENGTH = 1000 * 60 * 60 * 24 * 3; // 3 days diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index f2509105a..a236bf4e4 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -10,6 +10,7 @@ import { DEFAULT_SESSION_START_EVENT, MASK_TEXT_CLASS, MAX_EVENT_LIST_SIZE_IN_BYTES, + MAX_IDB_STORAGE_LENGTH, MAX_INTERVAL, MIN_INTERVAL, SESSION_REPLAY_SERVER_URL, @@ -151,8 +152,8 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } setShouldRecord(sessionStore?: IDBStoreSession) { - if (sessionStore?.shouldRecord === false) { - this.shouldRecord = false; + if (sessionStore && typeof sessionStore.shouldRecord === 'boolean') { + this.shouldRecord = sessionStore.shouldRecord; } else if (this.config.optOut) { this.shouldRecord = false; } else if (this.options && this.options.sampleRate) { @@ -324,7 +325,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { }; const res = await fetch(SESSION_REPLAY_SERVER_URL, options); if (res === null) { - this.completeRequest({ context, err: UNEXPECTED_ERROR_MESSAGE, removeEvents: false }); + this.completeRequest({ context, err: UNEXPECTED_ERROR_MESSAGE }); return; } if (!useRetry) { @@ -339,7 +340,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { this.handleReponse(res.status, context); } } catch (e) { - this.completeRequest({ context, err: e as string, removeEvents: false }); + this.completeRequest({ context, err: e as string }); } } @@ -431,6 +432,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { sequenceToUpdate.events = []; sequenceToUpdate.status = RecordingStatus.SENT; + // Delete sent sequences for current session Object.entries(session.sessionSequences).forEach(([storedSeqId, sequence]) => { const numericStoredSeqId = parseInt(storedSeqId, 10); if (sequence.status === RecordingStatus.SENT && sequenceId !== numericStoredSeqId) { @@ -438,6 +440,14 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } }); + // Delete any sessions that are older than 3 days + Object.keys(sessionMap).forEach((sessionId: string) => { + const numericSessionId = parseInt(sessionId, 10); + if (Date.now() - numericSessionId >= MAX_IDB_STORAGE_LENGTH) { + delete sessionMap[numericSessionId]; + } + }); + return sessionMap; }); } catch (e) { @@ -445,18 +455,8 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } } - completeRequest({ - context, - err, - success, - removeEvents = true, - }: { - context: SessionReplayContext; - err?: string; - success?: string; - removeEvents?: boolean; - }) { - removeEvents && context.sessionId && this.cleanUpSessionEventsStore(context.sessionId, context.sequenceId); + completeRequest({ context, err, success }: { context: SessionReplayContext; err?: string; success?: string }) { + context.sessionId && this.cleanUpSessionEventsStore(context.sessionId, context.sequenceId); if (err) { this.config.loggerProvider.error(err); } else if (success) { diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 27840a4aa..0af535de0 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -371,6 +371,18 @@ describe('SessionReplayPlugin', () => { sessionReplay.setShouldRecord(sessionStore); expect(sessionReplay.shouldRecord).toBe(false); }); + test('should set record as true if true in session store', () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + sessionReplay.shouldRecord = false; // Set to false to be sure the setShouldRecord changes the value + const sessionStore = { + shouldRecord: true, + currentSequenceId: 0, + sessionSequences: {}, + }; + sessionReplay.setShouldRecord(sessionStore); + expect(sessionReplay.shouldRecord).toBe(true); + }); test('should not set record as false if no options', () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; @@ -782,7 +794,7 @@ describe('SessionReplayPlugin', () => { expect(cleanUpSessionEventsStore).toHaveBeenCalledTimes(1); expect(cleanUpSessionEventsStore.mock.calls[0]).toEqual([123, 1]); }); - test('should not remove session events from IDB store upon failure', async () => { + test('should remove session events from IDB store upon failure', async () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; const context = { @@ -792,12 +804,15 @@ describe('SessionReplayPlugin', () => { attempts: 0, timeout: 0, }; + const cleanUpSessionEventsStore = jest + .spyOn(sessionReplay, 'cleanUpSessionEventsStore') + .mockReturnValueOnce(Promise.resolve()); (global.fetch as jest.Mock).mockImplementationOnce(() => Promise.reject()); await sessionReplay.send(context); jest.runAllTimers(); - - expect(update).not.toHaveBeenCalled(); + expect(cleanUpSessionEventsStore).toHaveBeenCalledTimes(1); + expect(cleanUpSessionEventsStore.mock.calls[0]).toEqual([123, 1]); }); test('should retry if retry param is true', async () => { @@ -1260,10 +1275,12 @@ describe('SessionReplayPlugin', () => { describe('cleanUpSessionEventsStore', () => { test('should update events and status for current session', async () => { + jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:30:00').getTime()); const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; + const currentSessionId = new Date('2023-07-31 07:30:00').getTime(); const mockIDBStore: IDBStore = { - 123: { + [currentSessionId]: { shouldRecord: true, currentSequenceId: 3, sessionSequences: { @@ -1277,22 +1294,12 @@ describe('SessionReplayPlugin', () => { }, }, }, - 456: { - shouldRecord: true, - currentSequenceId: 1, - sessionSequences: { - 1: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, }; - await sessionReplay.cleanUpSessionEventsStore(123, 3); + await sessionReplay.cleanUpSessionEventsStore(currentSessionId, 3); expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ - 123: { + [currentSessionId]: { shouldRecord: true, currentSequenceId: 3, sessionSequences: { @@ -1306,24 +1313,16 @@ describe('SessionReplayPlugin', () => { }, }, }, - 456: { - shouldRecord: true, - currentSequenceId: 1, - sessionSequences: { - 1: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, }); }); test('should delete sent sequences for current session', async () => { + jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:30:00').getTime()); const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; + const currentSessionId = new Date('2023-07-31 07:30:00').getTime(); const mockIDBStore: IDBStore = { - 123: { + [currentSessionId]: { shouldRecord: true, currentSequenceId: 3, sessionSequences: { @@ -1337,22 +1336,42 @@ describe('SessionReplayPlugin', () => { }, }, }, - 456: { + }; + await sessionReplay.cleanUpSessionEventsStore(currentSessionId, 3); + + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ + [currentSessionId]: { shouldRecord: true, - currentSequenceId: 1, + currentSequenceId: 3, sessionSequences: { - 1: { + 3: { events: [], status: RecordingStatus.SENT, }, }, }, - }; - await sessionReplay.cleanUpSessionEventsStore(123, 3); + }); + }); - expect(update).toHaveBeenCalledTimes(1); - expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ - 123: { + test('should keep sessions that are less than 3 days old, and delete sessions that are more than 3 days old', async () => { + jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:30:00').getTime()); + const fourDayOldSessionId = new Date('2023-07-27 07:30:00').getTime(); + const oneDayOldSessionId = new Date('2023-07-30 07:30:00').getTime(); + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + const mockIDBStore: IDBStore = { + [oneDayOldSessionId]: { + shouldRecord: true, + currentSequenceId: 3, + sessionSequences: { + 3: { + events: [mockEventString], + status: RecordingStatus.SENDING, + }, + }, + }, + [fourDayOldSessionId]: { shouldRecord: true, currentSequenceId: 3, sessionSequences: { @@ -1362,11 +1381,16 @@ describe('SessionReplayPlugin', () => { }, }, }, - 456: { + }; + await sessionReplay.cleanUpSessionEventsStore(oneDayOldSessionId, 3); + + expect(update).toHaveBeenCalledTimes(1); + expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ + [oneDayOldSessionId]: { shouldRecord: true, - currentSequenceId: 1, + currentSequenceId: 3, sessionSequences: { - 1: { + 3: { events: [], status: RecordingStatus.SENT, }, From b279f0df007ae0c8e7f8a97e12530af795c473bc Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Mon, 31 Jul 2023 17:41:31 +0000 Subject: [PATCH 082/214] chore(release): publish - @amplitude/plugin-user-agent-enrichment-browser@1.0.0 --- .../plugin-user-agent-enrichment-browser/CHANGELOG.md | 9 +++++++++ .../plugin-user-agent-enrichment-browser/package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md b/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md index 87379728b..cffa9af53 100644 --- a/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md +++ b/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.0.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-user-agent-enrichment-browser@1.0.0-beta.1...@amplitude/plugin-user-agent-enrichment-browser@1.0.0) (2023-07-31) + +**Note:** Version bump only for package @amplitude/plugin-user-agent-enrichment-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [1.0.0-beta.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-user-agent-enrichment-browser@0.1.0...@amplitude/plugin-user-agent-enrichment-browser@1.0.0-beta.1) (2023-07-31) **Note:** Version bump only for package @amplitude/plugin-user-agent-enrichment-browser diff --git a/packages/plugin-user-agent-enrichment-browser/package.json b/packages/plugin-user-agent-enrichment-browser/package.json index 5cd62c93b..f842130ad 100644 --- a/packages/plugin-user-agent-enrichment-browser/package.json +++ b/packages/plugin-user-agent-enrichment-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-user-agent-enrichment-browser", - "version": "1.0.0-beta.1", + "version": "1.0.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 016865335d05371849738403817410e2afc0ca57 Mon Sep 17 00:00:00 2001 From: Jackson Huang Date: Mon, 31 Jul 2023 15:21:49 -0700 Subject: [PATCH 083/214] feat(session replay eu): route rraffic to eu based on config --- .../src/constants.ts | 1 + .../src/session-replay.ts | 13 ++++++-- .../test/session-replay.test.ts | 30 ++++++++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/constants.ts b/packages/plugin-session-replay-browser/src/constants.ts index 55936ce9e..c014410bf 100644 --- a/packages/plugin-session-replay-browser/src/constants.ts +++ b/packages/plugin-session-replay-browser/src/constants.ts @@ -11,6 +11,7 @@ export const BLOCK_CLASS = 'amp-block'; export const MASK_TEXT_CLASS = 'amp-mask'; export const UNMASK_TEXT_CLASS = 'amp-unmask'; export const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/track'; +export const SESSION_REPLAY_EU_URL = 'https://api.eu.amplitude.com/sessions/track'; export const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 500; // derived by JSON stringifying an example payload without events export const MAX_EVENT_LIST_SIZE_IN_BYTES = 10 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index f2509105a..5073f7b4d 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -1,6 +1,6 @@ import { getGlobalScope } from '@amplitude/analytics-client-common'; import { BaseTransport } from '@amplitude/analytics-core'; -import { BrowserConfig, Event, Status } from '@amplitude/analytics-types'; +import { BrowserConfig, Event, ServerZone, Status } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import { pack, record } from 'rrweb'; import { @@ -15,6 +15,7 @@ import { SESSION_REPLAY_SERVER_URL, STORAGE_PREFIX, defaultSessionStore, + SESSION_REPLAY_EU_URL as SESSION_REPLAY_EU_SERVER_URL, } from './constants'; import { maskInputFn } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, UNEXPECTED_ERROR_MESSAGE, getSuccessMessage } from './messages'; @@ -301,6 +302,13 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { await Promise.all(list.map((context) => this.send(context, useRetry))); } + getServerUrl() { + if (this.config.serverZone === ServerZone.EU) { + return SESSION_REPLAY_EU_SERVER_URL; + } + return SESSION_REPLAY_SERVER_URL; + } + async send(context: SessionReplayContext, useRetry = true) { const payload = { api_key: this.config.apiKey, @@ -322,7 +330,8 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { body: JSON.stringify(payload), method: 'POST', }; - const res = await fetch(SESSION_REPLAY_SERVER_URL, options); + const server_url = this.getServerUrl(); + const res = await fetch(server_url, options); if (res === null) { this.completeRequest({ context, err: UNEXPECTED_ERROR_MESSAGE, removeEvents: false }); return; diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 27840a4aa..dc2fa1b66 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -1,7 +1,7 @@ /* eslint-disable jest/expect-expect */ import * as AnalyticsClientCommon from '@amplitude/analytics-client-common'; import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; -import { BrowserConfig, LogLevel, Logger } from '@amplitude/analytics-types'; +import { BrowserConfig, LogLevel, Logger, ServerZone } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import * as RRWeb from 'rrweb'; import { UNEXPECTED_ERROR_MESSAGE, getSuccessMessage } from '../src/messages'; @@ -764,6 +764,34 @@ describe('SessionReplayPlugin', () => { method: 'POST', }); }); + test('should make a request to eu', async () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = { + ...mockConfig, + serverZone: ServerZone.EU, + }; + + const context = { + events: [mockEventString], + sequenceId: 1, + sessionId: 123, + attempts: 0, + timeout: 0, + }; + + await sessionReplay.send(context); + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith('https://api.eu.amplitude.com/sessions/track', { + body: JSON.stringify({ + api_key: 'static_key', + session_id: 123, + start_timestamp: 123, + events_batch: { version: 1, events: [mockEventString], seq_number: 1 }, + }), + headers: { Accept: '*/*', 'Content-Type': 'application/json' }, + method: 'POST', + }); + }); test('should update IDB store upon success', async () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; From e641477064763d7dedf41bcd870aa149cccf1832 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Tue, 1 Aug 2023 16:19:38 +0000 Subject: [PATCH 084/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.4.2 --- packages/plugin-session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/plugin-session-replay-browser/package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 086db4822..988ab159a 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.4.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.4.1...@amplitude/plugin-session-replay-browser@0.4.2) (2023-08-01) + +### Bug Fixes + +- **plugins:** correctly fetch shouldRecord from store and clean up old store entries + ([1e8317b](https://github.com/amplitude/Amplitude-TypeScript/commit/1e8317b64d8868bd4b5e584f50ffec4ca54dcadc)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.4.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.4.0...@amplitude/plugin-session-replay-browser@0.4.1) (2023-07-27) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 5874170a3..9145d117d 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.4.1", + "version": "0.4.2", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 2857038310110b9bae57ad45295107895586a847 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Tue, 1 Aug 2023 16:31:27 -0400 Subject: [PATCH 085/214] fix(plugins): fix edge case when opening tab in background --- .../src/session-replay.ts | 11 +++++--- .../test/session-replay.test.ts | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 76e358aaa..fe18419f4 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -130,6 +130,12 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { return; } const storedReplaySessions = await this.getAllSessionEventsFromStore(); + // This resolves a timing issue when focus is fired multiple times in short succession, + // we only want the rest of this function to run once. We can be sure that initialize has + // already been called if this.stopRecordingEvents is defined + if (this.stopRecordingEvents) { + return; + } const storedSequencesForSession = storedReplaySessions && storedReplaySessions[this.config.sessionId]; if (storedReplaySessions && storedSequencesForSession && storedSequencesForSession.sessionSequences) { const storedSeqId = storedSequencesForSession.currentSequenceId; @@ -147,9 +153,8 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { if (shouldSendStoredEvents && storedReplaySessions) { this.sendStoredEvents(storedReplaySessions); } - if (!this.stopRecordingEvents) { - this.recordEvents(); - } + + this.recordEvents(); } setShouldRecord(sessionStore?: IDBStoreSession) { diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 920580e6d..a61465e58 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -248,6 +248,31 @@ describe('SessionReplayPlugin', () => { await sessionReplay.initialize(); expect(getAllSessionEventsFromStore).not.toHaveBeenCalled(); }); + test('should return early if stopRecordingEvents is already defined, signaling that recording is already in progress', async () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = mockConfig; + // eslint-disable-next-line @typescript-eslint/no-empty-function + sessionReplay.stopRecordingEvents = () => {}; + const getAllSessionEventsFromStore = jest + .spyOn(sessionReplay, 'getAllSessionEventsFromStore') + .mockReturnValueOnce( + Promise.resolve({ + 123: { + shouldRecord: true, + currentSequenceId: 3, + sessionSequences: { + 3: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, + }, + }), + ); + await sessionReplay.initialize(); + expect(getAllSessionEventsFromStore).toHaveBeenCalled(); + expect(sessionReplay.events).toEqual([]); // events should not be updated to match what is in the store + }); test('should configure current sequence id and events correctly if last sequence was sending', async () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; From a9293cb635a4c088ec928f2205f2d19cc47ec4e1 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Wed, 2 Aug 2023 00:38:08 +0000 Subject: [PATCH 086/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.5.0 --- .../plugin-session-replay-browser/CHANGELOG.md | 17 +++++++++++++++++ .../plugin-session-replay-browser/package.json | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 988ab159a..fadc4cf41 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.5.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.4.2...@amplitude/plugin-session-replay-browser@0.5.0) (2023-08-02) + +### Bug Fixes + +- **plugins:** fix edge case when opening tab in background + ([2857038](https://github.com/amplitude/Amplitude-TypeScript/commit/2857038310110b9bae57ad45295107895586a847)) + +### Features + +- **session replay eu:** route rraffic to eu based on config + ([0168653](https://github.com/amplitude/Amplitude-TypeScript/commit/016865335d05371849738403817410e2afc0ca57)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.4.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.4.1...@amplitude/plugin-session-replay-browser@0.4.2) (2023-08-01) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 9145d117d..46fb83ded 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.4.2", + "version": "0.5.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 4c7e5791b02114caf5ae95d7612fef0f339fbfee Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 3 Aug 2023 16:20:56 -0400 Subject: [PATCH 087/214] fix(plugins): use hash to calculate is session is included in sample --- .../src/constants.ts | 1 - .../src/helpers.ts | 19 ++ .../src/session-replay.ts | 62 ++--- .../src/typings/session-replay.ts | 5 +- .../test/helpers.test.ts | 28 +- .../test/session-replay.test.ts | 263 ++++-------------- 6 files changed, 118 insertions(+), 260 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/constants.ts b/packages/plugin-session-replay-browser/src/constants.ts index 2b1f5046c..8e02f45a8 100644 --- a/packages/plugin-session-replay-browser/src/constants.ts +++ b/packages/plugin-session-replay-browser/src/constants.ts @@ -18,7 +18,6 @@ export const MAX_EVENT_LIST_SIZE_IN_BYTES = 10 * 1000000 - PAYLOAD_ESTIMATED_SIZ export const MIN_INTERVAL = 500; // 500 ms export const MAX_INTERVAL = 10 * 1000; // 10 seconds export const defaultSessionStore: IDBStoreSession = { - shouldRecord: true, currentSequenceId: 0, sessionSequences: {}, }; diff --git a/packages/plugin-session-replay-browser/src/helpers.ts b/packages/plugin-session-replay-browser/src/helpers.ts index 3aeaaaf12..3070fb1ca 100644 --- a/packages/plugin-session-replay-browser/src/helpers.ts +++ b/packages/plugin-session-replay-browser/src/helpers.ts @@ -6,3 +6,22 @@ export const maskInputFn = (text: string, element: HTMLElement) => { } return '*'.repeat(text.length); }; + +export const generateHashCode = function (str: string) { + let hash = 0; + if (str.length === 0) return hash; + for (let i = 0; i < str.length; i++) { + const chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; + } + return hash; +}; + +export const isSessionInSample = function (sessionId: number, sampleRate: number) { + const hashNumber = generateHashCode(sessionId.toString()); + const absHash = Math.abs(hashNumber); + const absHashMultiply = absHash * 31; + const mod = absHashMultiply % 100; + return mod / 100 < sampleRate; +}; diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index fe18419f4..781062278 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -13,12 +13,12 @@ import { MAX_IDB_STORAGE_LENGTH, MAX_INTERVAL, MIN_INTERVAL, + SESSION_REPLAY_EU_URL as SESSION_REPLAY_EU_SERVER_URL, SESSION_REPLAY_SERVER_URL, STORAGE_PREFIX, defaultSessionStore, - SESSION_REPLAY_EU_URL as SESSION_REPLAY_EU_SERVER_URL, } from './constants'; -import { maskInputFn } from './helpers'; +import { isSessionInSample, maskInputFn } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, UNEXPECTED_ERROR_MESSAGE, getSuccessMessage } from './messages'; import { Events, @@ -48,7 +48,6 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { interval = MIN_INTERVAL; timeAtLastSend: number | null = null; options: SessionReplayOptions; - shouldRecord = true; constructor(options?: SessionReplayOptions) { this.options = { ...options }; @@ -98,15 +97,7 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { if (GlobalScope && GlobalScope.document && !GlobalScope.document.hasFocus()) { return Promise.resolve(event); } - - if (this.shouldRecord) { - event.event_properties = { - ...event.event_properties, - [DEFAULT_SESSION_REPLAY_PROPERTY]: true, - }; - } if (event.event_type === DEFAULT_SESSION_START_EVENT && !this.stopRecordingEvents) { - this.setShouldRecord(); this.recordEvents(); } else if (event.event_type === DEFAULT_SESSION_END_EVENT) { if (event.session_id && this.events.length) { @@ -121,6 +112,15 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { this.events = []; this.currentSequenceId = 0; } + + const shouldRecord = this.getShouldRecord(); + if (shouldRecord) { + event.event_properties = { + ...event.event_properties, + [DEFAULT_SESSION_REPLAY_PROPERTY]: true, + }; + } + return Promise.resolve(event); } @@ -149,27 +149,21 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { this.events = lastSequence?.events || []; } } - this.setShouldRecord(storedSequencesForSession); if (shouldSendStoredEvents && storedReplaySessions) { this.sendStoredEvents(storedReplaySessions); } - this.recordEvents(); } - setShouldRecord(sessionStore?: IDBStoreSession) { - if (sessionStore && typeof sessionStore.shouldRecord === 'boolean') { - this.shouldRecord = sessionStore.shouldRecord; - } else if (this.config.optOut) { - this.shouldRecord = false; + getShouldRecord() { + if (this.config.optOut) { + return false; + } else if (!this.config.sessionId) { + return false; } else if (this.options && this.options.sampleRate) { - this.shouldRecord = Math.random() < this.options.sampleRate; - } - - this.config.sessionId && void this.storeShouldRecordForSession(this.config.sessionId, this.shouldRecord); - if (!this.shouldRecord && this.config.sessionId) { - this.config.loggerProvider.log(`Opting session ${this.config.sessionId} out of recording.`); + return isSessionInSample(this.config.sessionId, this.options.sampleRate); } + return true; } sendStoredEvents(storedReplaySessions: IDBStore) { @@ -194,7 +188,9 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } recordEvents() { - if (!this.shouldRecord) { + const shouldRecord = this.getShouldRecord(); + if (!shouldRecord && this.config.sessionId) { + this.config.loggerProvider.log(`Opting session ${this.config.sessionId} out of recording.`); return; } this.stopRecordingEvents = record({ @@ -418,22 +414,6 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } } - async storeShouldRecordForSession(sessionId: number, shouldRecord: boolean) { - try { - await IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore = {}): IDBStore => { - const session: IDBStoreSession = sessionMap[sessionId] || { ...defaultSessionStore }; - session.shouldRecord = shouldRecord; - - return { - ...sessionMap, - [sessionId]: session, - }; - }); - } catch (e) { - this.config.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); - } - } - async cleanUpSessionEventsStore(sessionId: number, sequenceId: number) { try { await IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore = {}): IDBStore => { diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index d0b5ed359..5e2924a90 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -27,7 +27,6 @@ export interface IDBStoreSequence { } export interface IDBStoreSession { - shouldRecord: boolean; currentSequenceId: number; sessionSequences: { [sequenceId: number]: IDBStoreSequence; @@ -45,13 +44,12 @@ export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { events: Events; currentSequenceId: number; interval: number; - shouldRecord: boolean; queue: SessionReplayContext[]; timeAtLastSend: number | null; stopRecordingEvents: ReturnType | null; maxPersistedEventsSize: number; initialize: (shouldSendStoredEvents?: boolean) => Promise; - setShouldRecord: (sessionStore?: IDBStoreSession) => void; + getShouldRecord: () => boolean; recordEvents: () => void; shouldSplitEventsList: (nextEventString: string) => boolean; sendEventsList: ({ @@ -80,7 +78,6 @@ export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { }): void; getAllSessionEventsFromStore: () => Promise; storeEventsForSession: (events: Events, sequenceId: number, sessionId: number) => Promise; - storeShouldRecordForSession: (sessionId: number, shouldRecord: boolean) => Promise; cleanUpSessionEventsStore: (sessionId: number, sequenceId: number) => Promise; } diff --git a/packages/plugin-session-replay-browser/test/helpers.test.ts b/packages/plugin-session-replay-browser/test/helpers.test.ts index e2283874e..a51e63faa 100644 --- a/packages/plugin-session-replay-browser/test/helpers.test.ts +++ b/packages/plugin-session-replay-browser/test/helpers.test.ts @@ -1,5 +1,5 @@ import { UNMASK_TEXT_CLASS } from '../src/constants'; -import { maskInputFn } from '../src/helpers'; +import { generateHashCode, isSessionInSample, maskInputFn } from '../src/helpers'; describe('SessionReplayPlugin helpers', () => { describe('maskInputFn', () => { @@ -21,4 +21,30 @@ describe('SessionReplayPlugin helpers', () => { expect(result).toEqual('*********'); }); }); + + describe('generateHashCode', () => { + test('should return 0 if string length is 0', () => { + const hashCode = generateHashCode(''); + expect(hashCode).toEqual(0); + }); + test('should return hash for numeric string', () => { + const hashCode = generateHashCode('1691093770366'); + expect(hashCode).toEqual(139812688); + }); + test('should return hash for alphabetic string', () => { + const hashCode = generateHashCode('my_session_identifier'); + expect(hashCode).toEqual(989939557); + }); + }); + + describe('isSessionInSample', () => { + test('should deterministically return true if calculation puts session id below sample rate', () => { + const result = isSessionInSample(1691092433788, 0.5); + expect(result).toEqual(true); + }); + test('should deterministically return false if calculation puts session id above sample rate', () => { + const result = isSessionInSample(1691092416403, 0.5); + expect(result).toEqual(false); + }); + }); }); diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index a61465e58..297f8cf8c 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -4,6 +4,8 @@ import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-commo import { BrowserConfig, LogLevel, Logger, ServerZone } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import * as RRWeb from 'rrweb'; +import { DEFAULT_SESSION_REPLAY_PROPERTY } from '../src/constants'; +import * as Helpers from '../src/helpers'; import { UNEXPECTED_ERROR_MESSAGE, getSuccessMessage } from '../src/messages'; import { sessionReplayPlugin } from '../src/session-replay'; import { IDBStore, RecordingStatus } from '../src/typings/session-replay'; @@ -173,7 +175,6 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = config; const mockGetResolution: Promise = Promise.resolve({ 123: { - shouldRecord: true, currentSequenceId: 3, sessionSequences: { 3: { @@ -183,7 +184,6 @@ describe('SessionReplayPlugin', () => { }, }, 456: { - shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -278,7 +278,6 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockGetResolution: Promise = Promise.resolve({ 123: { - shouldRecord: true, currentSequenceId: 3, sessionSequences: { 3: { @@ -298,7 +297,6 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockGetResolution: Promise = Promise.resolve({ 123: { - shouldRecord: true, currentSequenceId: 3, sessionSequences: { 3: { @@ -327,7 +325,6 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockGetResolution = Promise.resolve({ 123: { - shouldRecord: true, currentSequenceId: 0, sessionSequences: {}, }, @@ -383,55 +380,30 @@ describe('SessionReplayPlugin', () => { }); }); - describe('setShouldRecord', () => { - test('should set record as false if false in session store', () => { + describe('getShouldRecord', () => { + test('should return true if no options', () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; - const sessionStore = { - shouldRecord: false, - currentSequenceId: 0, - sessionSequences: {}, - }; - expect(sessionReplay.shouldRecord).toBe(true); - sessionReplay.setShouldRecord(sessionStore); - expect(sessionReplay.shouldRecord).toBe(false); - }); - test('should set record as true if true in session store', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - sessionReplay.shouldRecord = false; // Set to false to be sure the setShouldRecord changes the value - const sessionStore = { - shouldRecord: true, - currentSequenceId: 0, - sessionSequences: {}, - }; - sessionReplay.setShouldRecord(sessionStore); - expect(sessionReplay.shouldRecord).toBe(true); - }); - test('should not set record as false if no options', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - sessionReplay.setShouldRecord(); - expect(sessionReplay.shouldRecord).toBe(true); + const shouldRecord = sessionReplay.getShouldRecord(); + expect(shouldRecord).toBe(true); }); - test('should set record as false if session not included in sample rate', () => { - jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); + test('should return false if session not included in sample rate', () => { + jest.spyOn(Helpers, 'isSessionInSample').mockImplementationOnce(() => false); const sessionReplay = sessionReplayPlugin({ sampleRate: 0.2, }); sessionReplay.config = mockConfig; - expect(sessionReplay.shouldRecord).toBe(true); - sessionReplay.setShouldRecord(); - expect(sessionReplay.shouldRecord).toBe(false); + const shouldRecord = sessionReplay.getShouldRecord(); + expect(shouldRecord).toBe(false); }); test('should set record as true if session is included in sample rate', () => { - jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); + jest.spyOn(Helpers, 'isSessionInSample').mockImplementationOnce(() => true); const sessionReplay = sessionReplayPlugin({ sampleRate: 0.8, }); sessionReplay.config = mockConfig; - sessionReplay.setShouldRecord(); - expect(sessionReplay.shouldRecord).toBe(true); + const shouldRecord = sessionReplay.getShouldRecord(); + expect(shouldRecord).toBe(true); }); test('should set record as false if opt out in config', () => { const sessionReplay = sessionReplayPlugin(); @@ -439,8 +411,17 @@ describe('SessionReplayPlugin', () => { ...mockConfig, optOut: true, }; - sessionReplay.setShouldRecord(); - expect(sessionReplay.shouldRecord).toBe(false); + const shouldRecord = sessionReplay.getShouldRecord(); + expect(shouldRecord).toBe(false); + }); + test('should set record as false if no session id', () => { + const sessionReplay = sessionReplayPlugin(); + sessionReplay.config = { + ...mockConfig, + sessionId: undefined, + }; + const shouldRecord = sessionReplay.getShouldRecord(); + expect(shouldRecord).toBe(false); }); test('opt out in config should override the sample rate', () => { jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); @@ -451,8 +432,8 @@ describe('SessionReplayPlugin', () => { ...mockConfig, optOut: true, }; - sessionReplay.setShouldRecord(); - expect(sessionReplay.shouldRecord).toBe(false); + const shouldRecord = sessionReplay.getShouldRecord(); + expect(shouldRecord).toBe(false); }); }); @@ -550,7 +531,6 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1]({})).toEqual({ 123: { - shouldRecord: true, currentSequenceId: 0, sessionSequences: { 0: { @@ -611,7 +591,6 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1]({})).toEqual({ 123: { - shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -926,105 +905,43 @@ describe('SessionReplayPlugin', () => { describe('module level integration', () => { describe('with a sample rate', () => { test('should not record session if excluded due to sampling', async () => { - jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); + jest.spyOn(Helpers, 'isSessionInSample').mockImplementation(() => false); const sessionReplay = sessionReplayPlugin({ sampleRate: 0.2, }); await sessionReplay.setup(mockConfig); - expect(update).toHaveBeenCalled(); - expect(update.mock.calls[0][1]({})).toEqual({ - 123: { - shouldRecord: false, - currentSequenceId: 0, - sessionSequences: {}, - }, - }); - update.mockClear(); - await sessionReplay.execute({ + const event = await sessionReplay.execute({ event_type: 'my_event', session_id: 123, }); + // Confirm Amplitude event is not tagged with Session Recorded + expect(event).not.toMatchObject({ + event_properties: { + [DEFAULT_SESSION_REPLAY_PROPERTY]: true, + }, + }); expect(record).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); - await sessionReplay.execute({ + const endEvent = await sessionReplay.execute({ event_type: 'session_end', session_id: 123, }); - await runScheduleTimers(); - expect(fetch).not.toHaveBeenCalled(); - }); - test('should recalculate whether to exclude session due to sample rate when start session fires', async () => { - jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.1); - const sessionReplay = sessionReplayPlugin({ - sampleRate: 0.2, - }); - await sessionReplay.setup(mockConfig); - expect(update).toHaveBeenCalled(); - expect(update.mock.calls[0][1]({})).toEqual({ - 123: { - shouldRecord: true, - currentSequenceId: 0, - sessionSequences: {}, - }, - }); - update.mockClear(); - // Record is called in setup, but we're not interested in that now - record.mockClear(); - // This will exclude session from sample rate - jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); - await sessionReplay.execute({ - event_type: 'session_start', - session_id: 123, - }); - expect(record).not.toHaveBeenCalled(); - expect(update).toHaveBeenCalled(); - expect(update.mock.calls[0][1]({})).toEqual({ - 123: { - shouldRecord: false, - currentSequenceId: 0, - sessionSequences: {}, + // Confirm Amplitude event is not tagged with Session Recorded + expect(endEvent).not.toMatchObject({ + event_properties: { + [DEFAULT_SESSION_REPLAY_PROPERTY]: true, }, }); - await sessionReplay.execute({ - event_type: 'session_end', - session_id: 123, - }); await runScheduleTimers(); expect(fetch).not.toHaveBeenCalled(); }); - test('should fetch whether to record session from IDB', async () => { - const sessionReplay = sessionReplayPlugin(); - const mockIDBStore = { - 123: { - shouldRecord: false, - currentSequenceId: 0, - sessionSequences: {}, - }, - }; - get.mockImplementationOnce(() => Promise.resolve(mockIDBStore)); - await sessionReplay.setup(mockConfig); - expect(record).not.toHaveBeenCalled(); - expect(update).toHaveBeenCalled(); - expect(update.mock.calls[0][1]({})).toEqual({ - 123: { - shouldRecord: false, - currentSequenceId: 0, - sessionSequences: {}, - }, - }); - await sessionReplay.execute({ - event_type: 'session_start', - session_id: 123, - }); - expect(record).not.toHaveBeenCalled(); - }); test('should record session if included due to sampling', async () => { + jest.spyOn(Helpers, 'isSessionInSample').mockImplementation(() => true); (fetch as jest.Mock).mockImplementationOnce(() => { return Promise.resolve({ status: 200, }); }); - jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); const sessionReplay = sessionReplayPlugin({ sampleRate: 0.8, }); @@ -1033,19 +950,27 @@ describe('SessionReplayPlugin', () => { loggerProvider: mockLoggerProvider, }; await sessionReplay.setup(config); - await sessionReplay.execute({ + const startEvent = await sessionReplay.execute({ event_type: 'session_start', session_id: 456, }); + // Confirm Amplitude event is tagged with Session Recorded + expect(startEvent?.event_properties).toMatchObject({ + [DEFAULT_SESSION_REPLAY_PROPERTY]: true, + }); // Log is called from setup, but that's not what we're testing here mockLoggerProvider.log.mockClear(); expect(record).toHaveBeenCalled(); const recordArg = record.mock.calls[0][0]; recordArg?.emit && recordArg?.emit(mockEvent); - await sessionReplay.execute({ + const endEvent = await sessionReplay.execute({ event_type: 'session_end', session_id: 456, }); + // Confirm Amplitude event is tagged with Session Recorded + expect(endEvent?.event_properties).toMatchObject({ + [DEFAULT_SESSION_REPLAY_PROPERTY]: true, + }); await runScheduleTimers(); expect(fetch).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/unbound-method @@ -1256,75 +1181,6 @@ describe('SessionReplayPlugin', () => { ); }); }); - describe('storeShouldRecordForSession', () => { - test('should store if should record session', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const mockIDBStore: IDBStore = { - 123: { - shouldRecord: true, - currentSequenceId: 3, - sessionSequences: { - 2: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - 456: { - shouldRecord: true, - currentSequenceId: 1, - sessionSequences: { - 1: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, - }; - await sessionReplay.storeShouldRecordForSession(456, false); - expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ - ...mockIDBStore, - 456: { - ...mockIDBStore[456], - shouldRecord: false, - }, - }); - }); - test('should catch errors', async () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - loggerProvider: mockLoggerProvider, - }; - sessionReplay.config = config; - update.mockImplementationOnce(() => Promise.reject('error')); - await sessionReplay.storeShouldRecordForSession(123, true); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( - 'Failed to store session replay events in IndexedDB: error', - ); - }); - test('should handle an undefined store', async () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - loggerProvider: mockLoggerProvider, - }; - sessionReplay.config = config; - update.mockImplementationOnce(() => Promise.resolve()); - await sessionReplay.storeShouldRecordForSession(123, true); - expect(update.mock.calls[0][1](undefined)).toEqual({ - 123: { - shouldRecord: true, - currentSequenceId: 0, - sessionSequences: {}, - }, - }); - }); - }); describe('cleanUpSessionEventsStore', () => { test('should update events and status for current session', async () => { @@ -1334,7 +1190,6 @@ describe('SessionReplayPlugin', () => { const currentSessionId = new Date('2023-07-31 07:30:00').getTime(); const mockIDBStore: IDBStore = { [currentSessionId]: { - shouldRecord: true, currentSequenceId: 3, sessionSequences: { 2: { @@ -1353,7 +1208,6 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ [currentSessionId]: { - shouldRecord: true, currentSequenceId: 3, sessionSequences: { 2: { @@ -1376,7 +1230,6 @@ describe('SessionReplayPlugin', () => { const currentSessionId = new Date('2023-07-31 07:30:00').getTime(); const mockIDBStore: IDBStore = { [currentSessionId]: { - shouldRecord: true, currentSequenceId: 3, sessionSequences: { 2: { @@ -1395,7 +1248,6 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ [currentSessionId]: { - shouldRecord: true, currentSequenceId: 3, sessionSequences: { 3: { @@ -1415,7 +1267,6 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockIDBStore: IDBStore = { [oneDayOldSessionId]: { - shouldRecord: true, currentSequenceId: 3, sessionSequences: { 3: { @@ -1425,7 +1276,6 @@ describe('SessionReplayPlugin', () => { }, }, [fourDayOldSessionId]: { - shouldRecord: true, currentSequenceId: 3, sessionSequences: { 3: { @@ -1440,7 +1290,6 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ [oneDayOldSessionId]: { - shouldRecord: true, currentSequenceId: 3, sessionSequences: { 3: { @@ -1486,7 +1335,6 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockIDBStore: IDBStore = { 123: { - shouldRecord: true, currentSequenceId: 2, sessionSequences: { 2: { @@ -1496,7 +1344,6 @@ describe('SessionReplayPlugin', () => { }, }, 456: { - shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1511,7 +1358,6 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ 123: { - shouldRecord: true, currentSequenceId: 2, sessionSequences: { 2: { @@ -1521,7 +1367,6 @@ describe('SessionReplayPlugin', () => { }, }, 456: { - shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1537,7 +1382,6 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockIDBStore: IDBStore = { 123: { - shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1547,7 +1391,6 @@ describe('SessionReplayPlugin', () => { }, }, 456: { - shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1562,7 +1405,6 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ 123: { - shouldRecord: true, currentSequenceId: 2, sessionSequences: { 1: { @@ -1576,7 +1418,6 @@ describe('SessionReplayPlugin', () => { }, }, 456: { - shouldRecord: true, currentSequenceId: 1, sessionSequences: { 1: { @@ -1592,7 +1433,6 @@ describe('SessionReplayPlugin', () => { sessionReplay.config = mockConfig; const mockIDBStore: IDBStore = { 123: { - shouldRecord: true, currentSequenceId: 2, sessionSequences: { 2: { @@ -1607,7 +1447,6 @@ describe('SessionReplayPlugin', () => { expect(update).toHaveBeenCalledTimes(1); expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ 123: { - shouldRecord: true, currentSequenceId: 2, sessionSequences: { 2: { @@ -1617,7 +1456,6 @@ describe('SessionReplayPlugin', () => { }, }, 456: { - shouldRecord: true, currentSequenceId: 0, sessionSequences: { 0: { @@ -1656,7 +1494,6 @@ describe('SessionReplayPlugin', () => { await sessionReplay.storeEventsForSession([mockEventString], 0, 456); expect(update.mock.calls[0][1](undefined)).toEqual({ 456: { - shouldRecord: true, currentSequenceId: 0, sessionSequences: { 0: { From ca2bca168a8982724cde32abd12cb40821ef69c6 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Fri, 4 Aug 2023 15:03:26 +0000 Subject: [PATCH 088/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.5.1 --- packages/plugin-session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/plugin-session-replay-browser/package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index fadc4cf41..e0517e05d 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.5.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.5.0...@amplitude/plugin-session-replay-browser@0.5.1) (2023-08-04) + +### Bug Fixes + +- **plugins:** use hash to calculate is session is included in sample + ([4c7e579](https://github.com/amplitude/Amplitude-TypeScript/commit/4c7e5791b02114caf5ae95d7612fef0f339fbfee)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [0.5.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.4.2...@amplitude/plugin-session-replay-browser@0.5.0) (2023-08-02) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 46fb83ded..8e8e9c408 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.5.0", + "version": "0.5.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 589991733621042a10e62d1996ccb6eab9fc6f33 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Fri, 4 Aug 2023 13:31:25 -0400 Subject: [PATCH 089/214] test(plugins): add test for sending events that are not current sessions current sequence --- .../src/typings/session-replay.ts | 2 +- .../test/session-replay.test.ts | 71 ++++++++++++++++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index 5e2924a90..dec1b8d8e 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -17,7 +17,6 @@ export interface SessionReplayContext { export enum RecordingStatus { RECORDING = 'recording', - SENDING = 'sending', SENT = 'sent', } @@ -49,6 +48,7 @@ export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { stopRecordingEvents: ReturnType | null; maxPersistedEventsSize: number; initialize: (shouldSendStoredEvents?: boolean) => Promise; + sendStoredEvents: (storedReplaySessions: IDBStore) => void; getShouldRecord: () => boolean; recordEvents: () => void; shouldSplitEventsList: (nextEventString: string) => boolean; diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 297f8cf8c..2c6d559e9 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -273,7 +273,7 @@ describe('SessionReplayPlugin', () => { expect(getAllSessionEventsFromStore).toHaveBeenCalled(); expect(sessionReplay.events).toEqual([]); // events should not be updated to match what is in the store }); - test('should configure current sequence id and events correctly if last sequence was sending', async () => { + test('should configure current sequence id and events correctly if last sequence was sent', async () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; const mockGetResolution: Promise = Promise.resolve({ @@ -282,7 +282,7 @@ describe('SessionReplayPlugin', () => { sessionSequences: { 3: { events: [mockEventString], - status: RecordingStatus.SENDING, + status: RecordingStatus.SENT, }, }, }, @@ -342,6 +342,71 @@ describe('SessionReplayPlugin', () => { await sessionReplay.initialize(); expect(record).toHaveBeenCalledTimes(1); }); + describe('sendStoredEvents', () => { + test('should send all recording sequences except the current sequence for the current session', () => { + const sessionReplay = sessionReplayPlugin(); + const config = { + ...mockConfig, + sessionId: 456, + }; + sessionReplay.config = config; + sessionReplay.currentSequenceId = 3; + const store: IDBStore = { + 123: { + currentSequenceId: 5, + sessionSequences: { + 3: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + 4: { + events: [], + status: RecordingStatus.SENT, + }, + 5: { + events: [mockEventString, mockEventString], + status: RecordingStatus.RECORDING, + }, + }, + }, + 456: { + currentSequenceId: 3, + sessionSequences: { + 1: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + 2: { + events: [], + status: RecordingStatus.SENT, + }, + 3: { + events: [mockEventString], + status: RecordingStatus.RECORDING, + }, + }, + }, + }; + const sendEventsList = jest.spyOn(sessionReplay, 'sendEventsList'); + sessionReplay.sendStoredEvents(store); + expect(sendEventsList).toHaveBeenCalledTimes(3); + expect(sendEventsList.mock.calls[0][0]).toEqual({ + events: [mockEventString], + sequenceId: 3, + sessionId: 123, + }); + expect(sendEventsList.mock.calls[1][0]).toEqual({ + events: [mockEventString, mockEventString], + sequenceId: 5, + sessionId: 123, + }); + expect(sendEventsList.mock.calls[2][0]).toEqual({ + events: [mockEventString], + sequenceId: 1, + sessionId: 456, + }); + }); + }); describe('defaultTracking', () => { test('should not change defaultTracking if its set to true', async () => { const sessionReplay = sessionReplayPlugin(); @@ -1271,7 +1336,7 @@ describe('SessionReplayPlugin', () => { sessionSequences: { 3: { events: [mockEventString], - status: RecordingStatus.SENDING, + status: RecordingStatus.RECORDING, }, }, }, From 8d4afd64a2e90604f5c990a9a3df73082850b963 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Mon, 7 Aug 2023 12:22:30 -0400 Subject: [PATCH 090/214] feat(plugins): add teardown functionality to session replay plugin --- .../src/session-replay.ts | 65 ++++++++++++------- .../test/session-replay.test.ts | 63 +++++++++++++++--- 2 files changed, 94 insertions(+), 34 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 781062278..9ba6ee4ba 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -76,39 +76,46 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { }; } - const GlobalScope = getGlobalScope(); - if (GlobalScope && GlobalScope.window) { - GlobalScope.window.addEventListener('blur', () => { - this.stopRecordingEvents && this.stopRecordingEvents(); - this.stopRecordingEvents = null; - }); - GlobalScope.window.addEventListener('focus', () => { - void this.initialize(); - }); + const globalScope = getGlobalScope(); + if (globalScope) { + globalScope.addEventListener('blur', this.blurListener); + globalScope.addEventListener('focus', this.focusListener); } - if (GlobalScope && GlobalScope.document && GlobalScope.document.hasFocus()) { + if (globalScope && globalScope.document && globalScope.document.hasFocus()) { await this.initialize(true); } } + blurListener = () => { + this.stopRecordingAndSendEvents(); + }; + focusListener = () => { + void this.initialize(); + }; + + stopRecordingAndSendEvents(sessionId?: number) { + this.stopRecordingEvents && this.stopRecordingEvents(); + this.stopRecordingEvents = null; + const sessionIdToSend = sessionId || this.config.sessionId; + if (this.events.length && sessionIdToSend) { + this.sendEventsList({ + events: this.events, + sequenceId: this.currentSequenceId, + sessionId: sessionIdToSend, + }); + } + } + async execute(event: Event) { - const GlobalScope = getGlobalScope(); - if (GlobalScope && GlobalScope.document && !GlobalScope.document.hasFocus()) { + const globalScope = getGlobalScope(); + if (globalScope && globalScope.document && !globalScope.document.hasFocus()) { return Promise.resolve(event); } if (event.event_type === DEFAULT_SESSION_START_EVENT && !this.stopRecordingEvents) { this.recordEvents(); } else if (event.event_type === DEFAULT_SESSION_END_EVENT) { - if (event.session_id && this.events.length) { - this.sendEventsList({ - events: this.events, - sequenceId: this.currentSequenceId, - sessionId: event.session_id, - }); - } - this.stopRecordingEvents && this.stopRecordingEvents(); - this.stopRecordingEvents = null; + this.stopRecordingAndSendEvents(event.session_id); this.events = []; this.currentSequenceId = 0; } @@ -195,9 +202,9 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { } this.stopRecordingEvents = record({ emit: (event) => { - const GlobalScope = getGlobalScope(); - if (GlobalScope && GlobalScope.document && !GlobalScope.document.hasFocus()) { - this.stopRecordingEvents && this.stopRecordingEvents(); + const globalScope = getGlobalScope(); + if (globalScope && globalScope.document && !globalScope.document.hasFocus()) { + this.stopRecordingAndSendEvents(); return; } const eventString = JSON.stringify(event); @@ -457,6 +464,16 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { this.config.loggerProvider.log(success); } } + + async teardown() { + const globalScope = getGlobalScope(); + if (globalScope) { + globalScope.removeEventListener('blur', this.blurListener); + globalScope.removeEventListener('focus', this.focusListener); + } + + this.stopRecordingAndSendEvents(); + } } export const sessionReplayPlugin: SessionReplayPlugin = (options?: SessionReplayOptions) => { diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 2c6d559e9..0eac71017 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -40,6 +40,7 @@ describe('SessionReplayPlugin', () => { let originalFetch: typeof global.fetch; let mockLoggerProvider: MockedLogger; let addEventListenerMock: jest.Mock; + let removeEventListenerMock: jest.Mock; const mockConfig: BrowserConfig = { apiKey: 'static_key', flushIntervalMillis: 0, @@ -82,14 +83,14 @@ describe('SessionReplayPlugin', () => { }), ) as jest.Mock; addEventListenerMock = jest.fn() as typeof addEventListenerMock; + removeEventListenerMock = jest.fn() as typeof removeEventListenerMock; jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue({ - window: { - addEventListener: addEventListenerMock, - } as unknown as Window, + addEventListener: addEventListenerMock, + removeEventListener: removeEventListenerMock, document: { hasFocus: () => true, }, - } as Window & typeof globalThis); + } as unknown as typeof globalThis); mockLoggerProvider = { error: jest.fn(), log: jest.fn(), @@ -506,10 +507,11 @@ describe('SessionReplayPlugin', () => { test('it should return event if document does not have focus', async () => { const sessionReplay = sessionReplayPlugin(); jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue({ + addEventListener: addEventListenerMock, document: { hasFocus: () => false, }, - } as typeof globalThis); + } as unknown as typeof globalThis); await sessionReplay.setup(mockConfig); const event = { event_type: 'event_type', @@ -667,12 +669,14 @@ describe('SessionReplayPlugin', () => { }); }); - test('should return early if document is not in focus', () => { + test('should stop recording and send events if document is not in focus', () => { const sessionReplay = sessionReplayPlugin(); sessionReplay.config = mockConfig; sessionReplay.recordEvents(); - sessionReplay.stopRecordingEvents = jest.fn(); + const stopRecordingMock = jest.fn(); + sessionReplay.stopRecordingEvents = stopRecordingMock; expect(sessionReplay.events).toEqual([]); + sessionReplay.events = [mockEventString]; // Add one event to list to trigger sending in stopRecordingAndSendEvents jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue({ document: { hasFocus: () => false, @@ -682,9 +686,15 @@ describe('SessionReplayPlugin', () => { const sendEventsListMock = jest.spyOn(sessionReplay, 'sendEventsList').mockImplementationOnce(() => {}); const recordArg = record.mock.calls[0][0]; recordArg?.emit && recordArg?.emit(mockEvent); - expect(sendEventsListMock).toHaveBeenCalledTimes(0); - expect(sessionReplay.stopRecordingEvents).toHaveBeenCalled(); - expect(sessionReplay.events).toEqual([]); + expect(sendEventsListMock).toHaveBeenCalledTimes(1); + expect(sendEventsListMock).toHaveBeenCalledWith({ + events: [mockEventString], + sequenceId: 0, + sessionId: 123, + }); + expect(stopRecordingMock).toHaveBeenCalled(); + expect(sessionReplay.stopRecordingEvents).toEqual(null); + expect(sessionReplay.events).toEqual([mockEventString]); // events should not change, emmitted event should be ignored }); }); @@ -1618,4 +1628,37 @@ describe('SessionReplayPlugin', () => { }); }); }); + + describe('teardown', () => { + test('should remove event listeners', async () => { + const sessionReplay = sessionReplayPlugin(); + await sessionReplay.setup(mockConfig); + await sessionReplay.teardown?.(); + expect(removeEventListenerMock).toHaveBeenCalledTimes(2); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(removeEventListenerMock.mock.calls[0][0]).toEqual('blur'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(removeEventListenerMock.mock.calls[1][0]).toEqual('focus'); + }); + + test('should stop recording and send any events in queue', async () => { + const sessionReplay = sessionReplayPlugin(); + await sessionReplay.setup(mockConfig); + const stopRecordingMock = jest.fn(); + sessionReplay.stopRecordingEvents = stopRecordingMock; + sessionReplay.events = [mockEventString]; + // eslint-disable-next-line @typescript-eslint/no-empty-function + const sendEventsListMock = jest.spyOn(sessionReplay, 'sendEventsList').mockImplementationOnce(() => {}); + await sessionReplay.teardown?.(); + expect(stopRecordingMock).toHaveBeenCalled(); + expect(sessionReplay.stopRecordingEvents).toBe(null); + expect(sendEventsListMock).toHaveBeenCalled(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(sendEventsListMock.mock.calls[0][0]).toEqual({ + events: [mockEventString], + sequenceId: 0, + sessionId: 123, + }); + }); + }); }); From 5b8d427f86450335d1d7d564fc44d36537ec1d7e Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Mon, 7 Aug 2023 17:52:40 +0000 Subject: [PATCH 091/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.6.0 --- packages/plugin-session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/plugin-session-replay-browser/package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index e0517e05d..f672b31b3 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.6.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.5.1...@amplitude/plugin-session-replay-browser@0.6.0) (2023-08-07) + +### Features + +- **plugins:** add teardown functionality to session replay plugin + ([8d4afd6](https://github.com/amplitude/Amplitude-TypeScript/commit/8d4afd64a2e90604f5c990a9a3df73082850b963)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.5.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.5.0...@amplitude/plugin-session-replay-browser@0.5.1) (2023-08-04) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 8e8e9c408..8411f5918 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.5.1", + "version": "0.6.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 31d5c353f38c04bd183f01fb8d00258d50e34f8b Mon Sep 17 00:00:00 2001 From: Alyssa Yu Date: Tue, 8 Aug 2023 15:27:37 -0700 Subject: [PATCH 092/214] feat: page view v1 enrichment plugin (#524) --- .../plugin-page-view-v1-enrichment/README.md | 66 ++++++++ .../jest.config.js | 10 ++ .../package.json | 56 +++++++ .../rollup.config.js | 6 + .../src/index.ts | 2 + .../src/page-view-v1-enrichment.ts | 51 ++++++ .../typings/page-view-v1-enrichment-plugin.ts | 6 + .../test/page-view-v1-enrichment.test.ts | 149 ++++++++++++++++++ .../tsconfig.es5.json | 10 ++ .../tsconfig.esm.json | 10 ++ .../tsconfig.json | 11 ++ 11 files changed, 377 insertions(+) create mode 100644 packages/plugin-page-view-v1-enrichment/README.md create mode 100644 packages/plugin-page-view-v1-enrichment/jest.config.js create mode 100644 packages/plugin-page-view-v1-enrichment/package.json create mode 100644 packages/plugin-page-view-v1-enrichment/rollup.config.js create mode 100644 packages/plugin-page-view-v1-enrichment/src/index.ts create mode 100644 packages/plugin-page-view-v1-enrichment/src/page-view-v1-enrichment.ts create mode 100644 packages/plugin-page-view-v1-enrichment/src/typings/page-view-v1-enrichment-plugin.ts create mode 100644 packages/plugin-page-view-v1-enrichment/test/page-view-v1-enrichment.test.ts create mode 100644 packages/plugin-page-view-v1-enrichment/tsconfig.es5.json create mode 100644 packages/plugin-page-view-v1-enrichment/tsconfig.esm.json create mode 100644 packages/plugin-page-view-v1-enrichment/tsconfig.json diff --git a/packages/plugin-page-view-v1-enrichment/README.md b/packages/plugin-page-view-v1-enrichment/README.md new file mode 100644 index 000000000..130c5b063 --- /dev/null +++ b/packages/plugin-page-view-v1-enrichment/README.md @@ -0,0 +1,66 @@ +

+ + + +
+

+ +# @amplitude/@amplitude/plugin-page-view-v1-enrichment-browser + +Official Browser SDK plugin for making page view event compatible with browser v1. + +## Installation + +This package is published on NPM registry and is available to be installed using npm and yarn. + +```sh +# npm +npm install @amplitude/plugin-page-view-v1-enrichment-browser@^1.0.0 + +# yarn +yarn add @amplitude/plugin-page-view-v1-enrichment-browser@^1.0.0 +``` + +## Usage + +This plugin works on top of the Amplitude Browser SDK. It's used for updating the page view event_type and event_properties the same as Browser v1. +In Browser v2.x, we have enriched the page view event_type and event_propertis. You can use this plugin to maintain consistency with page view tracking from earlier SDK versions. + +### 1. Import Amplitude packages + +* `@amplitude/analytics-browser` +* `@amplitude/plugin-page-view-v1-enrichment-browser` + +```typescript +import * as amplitude from '@amplitude/analytics-browser'; +import { pageViewV1EnrichmentPlugin } from '@amplitude/plugin-page-view-v1-enrichment-browser'; +``` + +### 2. Instantiate page view v1 enrichment plugin + +```typescript +const pageViewPlugin = pageViewV1EnrichmentPlugin(); +``` + +### 3. Install plugin to Amplitude SDK + +```typescript +amplitude.add(pageViewPlugin); +``` + +### 4. Initialize Amplitude SDK + +```typescript +amplitude.init('API_KEY'); +``` + +## Resulting on page view event + +| property |[Browser SDK 2.0](../) | With this plugin | +| --- | --- | --- | +| `Event Type` | `[Amplitude] Page Viewed` | `Page View` | +| `Event Properties` | `page_domain` | `[Amplitude] Page Domain` | +| | `page_location` | `[Amplitude] Page Location` | +| | `page_path` | `[Amplitude] Page Path` | +| | `page_title` | `[Amplitude] Page Title` | +| | `page_url` | `[Amplitude] Page URL`| diff --git a/packages/plugin-page-view-v1-enrichment/jest.config.js b/packages/plugin-page-view-v1-enrichment/jest.config.js new file mode 100644 index 000000000..a0ef09957 --- /dev/null +++ b/packages/plugin-page-view-v1-enrichment/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../jest.config.js'); +const package = require('./package'); + +module.exports = { + ...baseConfig, + displayName: package.name, + rootDir: '.', + testEnvironment: 'jsdom', + coveragePathIgnorePatterns: ['index.ts'], +}; \ No newline at end of file diff --git a/packages/plugin-page-view-v1-enrichment/package.json b/packages/plugin-page-view-v1-enrichment/package.json new file mode 100644 index 000000000..e54733d9c --- /dev/null +++ b/packages/plugin-page-view-v1-enrichment/package.json @@ -0,0 +1,56 @@ +{ + "name": "@amplitude/plugin-page-view-v1-enrichment-browser", + "version": "0.0.0", + "description": "", + "author": "Amplitude Inc", + "homepage": "https://github.com/amplitude/Amplitude-TypeScript", + "license": "MIT", + "main": "lib/cjs/index.js", + "module": "lib/esm/index.js", + "types": "lib/esm/index.d.ts", + "sideEffects": false, + "publishConfig": { + "access": "public", + "tag": "latest" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/amplitude/Amplitude-TypeScript.git" + }, + "scripts": { + "build": "yarn bundle && yarn build:es5 && yarn build:esm", + "bundle": "rollup --config rollup.config.js", + "build:es5": "tsc -p ./tsconfig.es5.json", + "build:esm": "tsc -p ./tsconfig.esm.json", + "clean": "rimraf node_modules lib coverage", + "fix": "yarn fix:eslint & yarn fix:prettier", + "fix:eslint": "eslint '{src,test}/**/*.ts' --fix", + "fix:prettier": "prettier --write \"{src,test}/**/*.ts\"", + "lint": "yarn lint:eslint & yarn lint:prettier", + "lint:eslint": "eslint '{src,test}/**/*.ts'", + "lint:prettier": "prettier --check \"{src,test}/**/*.ts\"", + "test": "jest", + "typecheck": "tsc -p ./tsconfig.json" + }, + "bugs": { + "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" + }, + "dependencies": { + "@amplitude/analytics-client-common": "^2.0.4", + "@amplitude/analytics-types": "^2.1.1", + "tslib": "^2.4.1" + }, + "devDependencies": { + "@amplitude/analytics-browser": "^2.1.2", + "@rollup/plugin-commonjs": "^23.0.4", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^10.0.1", + "rollup": "^2.79.1", + "rollup-plugin-execute": "^1.1.1", + "rollup-plugin-gzip": "^3.1.0", + "rollup-plugin-terser": "^7.0.2" + }, + "files": [ + "lib" + ] +} diff --git a/packages/plugin-page-view-v1-enrichment/rollup.config.js b/packages/plugin-page-view-v1-enrichment/rollup.config.js new file mode 100644 index 000000000..0f6642c86 --- /dev/null +++ b/packages/plugin-page-view-v1-enrichment/rollup.config.js @@ -0,0 +1,6 @@ +import { iife, umd } from '../../scripts/build/rollup.config'; + +iife.input = umd.input; +iife.output.name = 'amplitudePageViewV1EnrichmentPlugin'; + +export default [umd, iife]; diff --git a/packages/plugin-page-view-v1-enrichment/src/index.ts b/packages/plugin-page-view-v1-enrichment/src/index.ts new file mode 100644 index 000000000..bf1863a2c --- /dev/null +++ b/packages/plugin-page-view-v1-enrichment/src/index.ts @@ -0,0 +1,2 @@ +export { pageViewV1EnrichmentPlugin } from './page-view-v1-enrichment'; +export { pageViewV1EnrichmentPlugin as plugin } from './page-view-v1-enrichment'; diff --git a/packages/plugin-page-view-v1-enrichment/src/page-view-v1-enrichment.ts b/packages/plugin-page-view-v1-enrichment/src/page-view-v1-enrichment.ts new file mode 100644 index 000000000..262ba3b48 --- /dev/null +++ b/packages/plugin-page-view-v1-enrichment/src/page-view-v1-enrichment.ts @@ -0,0 +1,51 @@ +import { BrowserConfig, EnrichmentPlugin, Event } from '@amplitude/analytics-types'; +import { CreatePageViewV1EnrichmentPlugin } from './typings/page-view-v1-enrichment-plugin'; + +export const pageViewV1EnrichmentPlugin: CreatePageViewV1EnrichmentPlugin = function () { + const plugin: EnrichmentPlugin = { + name: '@amplitude/plugin-page-view-v1-enrichment-browser', + type: 'enrichment', + + setup: async function (config: BrowserConfig) { + config.loggerProvider.log('Installing @amplitude/plugin-page-view-v1-enrichment-browser.'); + }, + + execute: async (event: Event) => { + if (event.event_type == '[Amplitude] Page Viewed') { + const { + '[Amplitude] Page Domain': page_domain, + '[Amplitude] Page Location': page_location, + '[Amplitude] Page Path': page_path, + '[Amplitude] Page Title': page_title, + '[Amplitude] Page URL': page_url, + } = event.event_properties || {}; + + event = { + ...event, + event_type: 'Page View', + event_properties: { + ...event.event_properties, + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ + page_domain: page_domain ?? '', + page_location: page_location ?? '', + page_path: page_path ?? '', + page_title: page_title ?? '', + page_url: page_url ?? '', + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ + }, + }; + + if (event.event_properties) { + delete event.event_properties['[Amplitude] Page Domain']; + delete event.event_properties['[Amplitude] Page Location']; + delete event.event_properties['[Amplitude] Page Path']; + delete event.event_properties['[Amplitude] Page Title']; + delete event.event_properties['[Amplitude] Page URL']; + } + } + return event; + }, + }; + + return plugin; +}; diff --git a/packages/plugin-page-view-v1-enrichment/src/typings/page-view-v1-enrichment-plugin.ts b/packages/plugin-page-view-v1-enrichment/src/typings/page-view-v1-enrichment-plugin.ts new file mode 100644 index 000000000..bb9ca6969 --- /dev/null +++ b/packages/plugin-page-view-v1-enrichment/src/typings/page-view-v1-enrichment-plugin.ts @@ -0,0 +1,6 @@ +/* eslint-disable @typescript-eslint/no-empty-interface */ +import { EnrichmentPlugin } from '@amplitude/analytics-types'; + +export interface CreatePageViewV1EnrichmentPlugin { + (): EnrichmentPlugin; +} diff --git a/packages/plugin-page-view-v1-enrichment/test/page-view-v1-enrichment.test.ts b/packages/plugin-page-view-v1-enrichment/test/page-view-v1-enrichment.test.ts new file mode 100644 index 000000000..9dca41bea --- /dev/null +++ b/packages/plugin-page-view-v1-enrichment/test/page-view-v1-enrichment.test.ts @@ -0,0 +1,149 @@ +import { createInstance } from '@amplitude/analytics-browser'; +import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; +import { pageViewV1EnrichmentPlugin } from '../src/page-view-v1-enrichment'; +import { BaseEvent, BrowserConfig, EnrichmentPlugin, LogLevel } from '@amplitude/analytics-types'; +import { Logger, UUID } from '@amplitude/analytics-core'; + +describe('page-view-v1-enrichment', () => { + let plugin: EnrichmentPlugin; + + const mockConfig: BrowserConfig = { + apiKey: UUID(), + flushIntervalMillis: 0, + flushMaxRetries: 0, + flushQueueSize: 0, + logLevel: LogLevel.None, + loggerProvider: new Logger(), + optOut: false, + serverUrl: undefined, + transportProvider: new FetchTransport(), + useBatch: false, + cookieOptions: { + domain: '.amplitude.com', + expiration: 365, + sameSite: 'Lax', + secure: false, + upgrade: true, + }, + cookieStorage: new CookieStorage(), + sessionTimeout: 30 * 60 * 1000, + trackingOptions: { + ipAddress: true, + language: true, + platform: true, + }, + }; + + beforeEach(() => { + plugin = pageViewV1EnrichmentPlugin(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('execute', () => { + test('should overwirte the browser v2 page view keys to v1 page view keys', async () => { + const event: BaseEvent = { + event_type: '[Amplitude] Page Viewed', + event_properties: { + '[Amplitude] Page Domain': 'page_domain_v2', + '[Amplitude] Page Path': 'page_path_v2', + '[Amplitude] Page Title': 'page_title_v2', + '[Amplitude] Page URL': 'page_url_v2', + '[Amplitude] Page Location': 'page_location_v2', + }, + user_properties: { + org: 'engineer', + }, + }; + + const amplitude = createInstance(); + + const executeSpy = jest.spyOn(plugin, 'execute'); + + await plugin.setup?.(mockConfig, amplitude); + const enrichedEvent = await plugin.execute?.(event); + + expect(executeSpy).toHaveBeenCalledWith(event); + expect(enrichedEvent?.event_type).toEqual('Page View'); + expect(enrichedEvent?.event_properties).toHaveProperty('page_domain'); + expect(enrichedEvent?.event_properties).toHaveProperty('page_location'); + expect(enrichedEvent?.event_properties).toHaveProperty('page_path'); + expect(enrichedEvent?.event_properties).toHaveProperty('page_title'); + expect(enrichedEvent?.event_properties).toHaveProperty('page_url'); + expect(enrichedEvent?.event_properties).toEqual({ + page_domain: 'page_domain_v2', + page_location: 'page_location_v2', + page_path: 'page_path_v2', + page_title: 'page_title_v2', + page_url: 'page_url_v2', + }); + + expect(enrichedEvent?.event_properties).not.toHaveProperty('[Amplitude] Page Domain'); + expect(enrichedEvent?.event_properties).not.toHaveProperty('[Amplitude] Page Location'); + expect(enrichedEvent?.event_properties).not.toHaveProperty('[Amplitude] Page Path'); + expect(enrichedEvent?.event_properties).not.toHaveProperty('[Amplitude] Page Title'); + expect(enrichedEvent?.event_properties).not.toHaveProperty('[Amplitude] Page URL'); + + expect(enrichedEvent?.user_properties).toEqual({ + org: 'engineer', + }); + }); + + test('should not throw error with imcomplete browser v2 page view', async () => { + const event = { + event_type: '[Amplitude] Page Viewed', + }; + + const amplitude = createInstance(); + + const executeSpy = jest.spyOn(plugin, 'execute'); + + await plugin.setup?.(mockConfig, amplitude); + const enrichedEvent = await plugin.execute?.(event); + + expect(executeSpy).toHaveBeenCalledWith(event); + expect(enrichedEvent?.event_type).toEqual('Page View'); + expect(enrichedEvent?.event_properties).toHaveProperty('page_domain'); + expect(enrichedEvent?.event_properties).toHaveProperty('page_path'); + expect(enrichedEvent?.event_properties).toHaveProperty('page_title'); + expect(enrichedEvent?.event_properties).toHaveProperty('page_url'); + + expect(enrichedEvent?.event_properties).toEqual({ + page_domain: '', + page_location: '', + page_path: '', + page_title: '', + page_url: '', + }); + + expect(enrichedEvent?.event_properties).not.toHaveProperty('[Amplitude] Page Domain'); + expect(enrichedEvent?.event_properties).not.toHaveProperty('[Amplitude] Page Location'); + expect(enrichedEvent?.event_properties).not.toHaveProperty('[Amplitude] Page Path'); + expect(enrichedEvent?.event_properties).not.toHaveProperty('[Amplitude] Page Title'); + expect(enrichedEvent?.event_properties).not.toHaveProperty('[Amplitude] Page URL'); + }); + + test('should not overwirte with no page view event', async () => { + const event = { + event_type: 'event_type', + event_properties: {}, + }; + + const amplitude = createInstance(); + + const executeSpy = jest.spyOn(plugin, 'execute'); + + await plugin.setup?.(mockConfig, amplitude); + const enrichedEvent = await plugin.execute?.(event); + + expect(executeSpy).toHaveBeenCalledWith(event); + expect(enrichedEvent?.event_type).toEqual('event_type'); + expect(enrichedEvent?.event_properties).not.toHaveProperty('page_domain'); + expect(enrichedEvent?.event_properties).not.toHaveProperty('page_path'); + expect(enrichedEvent?.event_properties).not.toHaveProperty('page_title'); + expect(enrichedEvent?.event_properties).not.toHaveProperty('page_url'); + }); + }); +}); diff --git a/packages/plugin-page-view-v1-enrichment/tsconfig.es5.json b/packages/plugin-page-view-v1-enrichment/tsconfig.es5.json new file mode 100644 index 000000000..77e041d3f --- /dev/null +++ b/packages/plugin-page-view-v1-enrichment/tsconfig.es5.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"], + "compilerOptions": { + "module": "commonjs", + "noEmit": false, + "outDir": "lib/cjs", + "rootDir": "./src" + } +} diff --git a/packages/plugin-page-view-v1-enrichment/tsconfig.esm.json b/packages/plugin-page-view-v1-enrichment/tsconfig.esm.json new file mode 100644 index 000000000..bec981eee --- /dev/null +++ b/packages/plugin-page-view-v1-enrichment/tsconfig.esm.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"], + "compilerOptions": { + "module": "es6", + "noEmit": false, + "outDir": "lib/esm", + "rootDir": "./src" + } +} diff --git a/packages/plugin-page-view-v1-enrichment/tsconfig.json b/packages/plugin-page-view-v1-enrichment/tsconfig.json new file mode 100644 index 000000000..bd2a4cdb7 --- /dev/null +++ b/packages/plugin-page-view-v1-enrichment/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*", "test/**/*"], + "compilerOptions": { + "baseUrl": ".", + "esModuleInterop": true, + "lib": ["dom"], + "noEmit": true, + "rootDir": ".", + } +} \ No newline at end of file From f9bb21699dd3d44dbca3318141249c3a57ce1000 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Tue, 8 Aug 2023 23:35:17 +0000 Subject: [PATCH 093/214] chore(release): publish - @amplitude/plugin-page-view-v1-enrichment-browser@0.1.0 --- packages/plugin-page-view-v1-enrichment/CHANGELOG.md | 11 +++++++++++ packages/plugin-page-view-v1-enrichment/package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 packages/plugin-page-view-v1-enrichment/CHANGELOG.md diff --git a/packages/plugin-page-view-v1-enrichment/CHANGELOG.md b/packages/plugin-page-view-v1-enrichment/CHANGELOG.md new file mode 100644 index 000000000..5e4bcb485 --- /dev/null +++ b/packages/plugin-page-view-v1-enrichment/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.1.0 (2023-08-08) + +### Features + +- page view v1 enrichment plugin ([#524](https://github.com/amplitude/Amplitude-TypeScript/issues/524)) + ([31d5c35](https://github.com/amplitude/Amplitude-TypeScript/commit/31d5c353f38c04bd183f01fb8d00258d50e34f8b)) diff --git a/packages/plugin-page-view-v1-enrichment/package.json b/packages/plugin-page-view-v1-enrichment/package.json index e54733d9c..ed478fd54 100644 --- a/packages/plugin-page-view-v1-enrichment/package.json +++ b/packages/plugin-page-view-v1-enrichment/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-page-view-v1-enrichment-browser", - "version": "0.0.0", + "version": "0.1.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From bb746e8b8da28180ab9750acc1356e182886d5ac Mon Sep 17 00:00:00 2001 From: Alyssa Yu Date: Wed, 9 Aug 2023 13:02:20 -0700 Subject: [PATCH 094/214] chore: add publish script to plugins to upload the script into cdn (#527) --- packages/plugin-page-view-v1-enrichment/package.json | 1 + packages/plugin-user-agent-enrichment-browser/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/plugin-page-view-v1-enrichment/package.json b/packages/plugin-page-view-v1-enrichment/package.json index ed478fd54..82091597f 100644 --- a/packages/plugin-page-view-v1-enrichment/package.json +++ b/packages/plugin-page-view-v1-enrichment/package.json @@ -29,6 +29,7 @@ "lint": "yarn lint:eslint & yarn lint:prettier", "lint:eslint": "eslint '{src,test}/**/*.ts'", "lint:prettier": "prettier --check \"{src,test}/**/*.ts\"", + "publish": "node ../../scripts/publish/upload-to-s3.js", "test": "jest", "typecheck": "tsc -p ./tsconfig.json" }, diff --git a/packages/plugin-user-agent-enrichment-browser/package.json b/packages/plugin-user-agent-enrichment-browser/package.json index f842130ad..0ebe4d58a 100644 --- a/packages/plugin-user-agent-enrichment-browser/package.json +++ b/packages/plugin-user-agent-enrichment-browser/package.json @@ -29,6 +29,7 @@ "lint": "yarn lint:eslint & yarn lint:prettier", "lint:eslint": "eslint '{src,test}/**/*.ts'", "lint:prettier": "prettier --check \"{src,test}/**/*.ts\"", + "publish": "node ../../scripts/publish/upload-to-s3.js", "test": "jest", "typecheck": "tsc -p ./tsconfig.json" }, From a95285028064aa75053b8db84137a2acf91a018f Mon Sep 17 00:00:00 2001 From: Alyssa Yu Date: Wed, 9 Aug 2023 13:06:47 -0700 Subject: [PATCH 095/214] chore: plugin-page-view-v1-enrichment@1.0.0-beta.0 (#526) --- packages/plugin-page-view-v1-enrichment/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-page-view-v1-enrichment/package.json b/packages/plugin-page-view-v1-enrichment/package.json index 82091597f..bb0d13b2b 100644 --- a/packages/plugin-page-view-v1-enrichment/package.json +++ b/packages/plugin-page-view-v1-enrichment/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-page-view-v1-enrichment-browser", - "version": "0.1.0", + "version": "1.0.0-beta.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 2f2b22af6d29531292ef3b331ce816196095bb2b Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Wed, 9 Aug 2023 21:05:47 +0000 Subject: [PATCH 096/214] chore(release): publish - @amplitude/plugin-page-view-v1-enrichment-browser@1.0.0 - @amplitude/plugin-user-agent-enrichment-browser@1.0.1 --- packages/plugin-page-view-v1-enrichment/CHANGELOG.md | 9 +++++++++ packages/plugin-page-view-v1-enrichment/package.json | 2 +- .../plugin-user-agent-enrichment-browser/CHANGELOG.md | 9 +++++++++ .../plugin-user-agent-enrichment-browser/package.json | 2 +- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/plugin-page-view-v1-enrichment/CHANGELOG.md b/packages/plugin-page-view-v1-enrichment/CHANGELOG.md index 5e4bcb485..b79ee917b 100644 --- a/packages/plugin-page-view-v1-enrichment/CHANGELOG.md +++ b/packages/plugin-page-view-v1-enrichment/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.0.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-v1-enrichment-browser@0.1.0...@amplitude/plugin-page-view-v1-enrichment-browser@1.0.0) (2023-08-09) + +**Note:** Version bump only for package @amplitude/plugin-page-view-v1-enrichment-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # 0.1.0 (2023-08-08) ### Features diff --git a/packages/plugin-page-view-v1-enrichment/package.json b/packages/plugin-page-view-v1-enrichment/package.json index bb0d13b2b..7a6be5ce6 100644 --- a/packages/plugin-page-view-v1-enrichment/package.json +++ b/packages/plugin-page-view-v1-enrichment/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-page-view-v1-enrichment-browser", - "version": "1.0.0-beta.0", + "version": "1.0.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", diff --git a/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md b/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md index cffa9af53..8ae3a6575 100644 --- a/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md +++ b/packages/plugin-user-agent-enrichment-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-user-agent-enrichment-browser@1.0.0...@amplitude/plugin-user-agent-enrichment-browser@1.0.1) (2023-08-09) + +**Note:** Version bump only for package @amplitude/plugin-user-agent-enrichment-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [1.0.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-user-agent-enrichment-browser@1.0.0-beta.1...@amplitude/plugin-user-agent-enrichment-browser@1.0.0) (2023-07-31) **Note:** Version bump only for package @amplitude/plugin-user-agent-enrichment-browser diff --git a/packages/plugin-user-agent-enrichment-browser/package.json b/packages/plugin-user-agent-enrichment-browser/package.json index 0ebe4d58a..3c704e971 100644 --- a/packages/plugin-user-agent-enrichment-browser/package.json +++ b/packages/plugin-user-agent-enrichment-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-user-agent-enrichment-browser", - "version": "1.0.0", + "version": "1.0.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 586cbc59dca0da88dd1564b93cb8cc5c8ac7796d Mon Sep 17 00:00:00 2001 From: Andrey Sokolov Date: Thu, 10 Aug 2023 23:53:31 +0400 Subject: [PATCH 097/214] fix: migrate legacy data (#486) * fix: migrate legacy data (Android) * chore: fix tests * fix: do not move legacy identifies if not first run since upgrade * chore: eslint * fix: get max event id from legacy events (for ios) * fix: put session data (Android) * fix: legacy database storage and native 'get' methods for iOS * fix: native 'remove' methods for iOS * fix: native 'remove' methods for Android * fix: remove legacy events after migration * fix: squash native methods * fix: changes in Android part * fix: fixes for iOS * chore: increase test coverage * fix: catch/log native Android exceptions * fix: example application to test legacy data migration (Android) * fix: example application to test legacy data migration (iOS) * fix: fix the example application (Android) * fix: review fixes * fix: fix getLastEventId() logic for iOS * chore: fix event prefixes in check messages --- .../legacyDataMigration/.bundle/config | 2 + .../legacyDataMigration/.eslintrc.js | 4 + .../legacyDataMigration/.gitignore | 66 + .../legacyDataMigration/.prettierrc.js | 7 + .../legacyDataMigration/.watchmanconfig | 1 + .../react-native/legacyDataMigration/App.tsx | 99 + .../react-native/legacyDataMigration/Gemfile | 6 + .../legacyDataMigration/MigrationChecker.ts | 133 + .../legacyDataMigration/README.md | 79 + .../__tests__/App.test.tsx | 17 + .../android/app/build.gradle | 125 + .../android/app/debug.keystore | Bin 0 -> 2257 bytes .../android/app/proguard-rules.pro | 10 + .../android/app/src/debug/AndroidManifest.xml | 13 + .../ReactNativeFlipper.java | 75 + .../android/app/src/main/AndroidManifest.xml | 25 + .../java/com/legacydatamigration/LegacyV3.kt | 369 + .../java/com/legacydatamigration/LegacyV4.kt | 405 + .../com/legacydatamigration/MainActivity.kt | 28 + .../legacydatamigration/MainApplication.kt | 47 + .../com/legacydatamigration/MainPackage.kt | 18 + .../legacydatamigration/MigrationModule.kt | 29 + .../res/drawable/rn_edit_text_material.xml | 36 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 9 + .../ReactNativeFlipper.java | 20 + .../legacyDataMigration/android/build.gradle | 23 + .../android/gradle.properties | 44 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 61574 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + .../legacyDataMigration/android/gradlew | 244 + .../legacyDataMigration/android/gradlew.bat | 92 + .../android/settings.gradle | 4 + .../react-native/legacyDataMigration/app.json | 4 + .../legacyDataMigration/babel.config.js | 3 + .../react-native/legacyDataMigration/index.js | 9 + .../legacyDataMigration/ios/.xcode.env | 11 + .../legacyDataMigration/ios/LegacyV3.swift | 332 + .../legacyDataMigration/ios/LegacyV4.swift | 332 + .../legacyDataMigration/ios/MigrationModule.m | 6 + .../ios/MigrationModule.swift | 59 + .../legacyDataMigration/ios/Podfile | 62 + .../legacyDataMigration/ios/Podfile.lock | 725 ++ .../ios/legacyDataMigration-Bridging-Header.h | 1 + .../project.pbxproj | 728 ++ .../xcschemes/legacyDataMigration.xcscheme | 88 + .../ios/legacyDataMigration/AppDelegate.h | 6 + .../ios/legacyDataMigration/AppDelegate.mm | 26 + .../AppIcon.appiconset/Contents.json | 53 + .../Images.xcassets/Contents.json | 6 + .../ios/legacyDataMigration/Info.plist | 55 + .../LaunchScreen.storyboard | 47 + .../ios/legacyDataMigration/main.m | 10 + .../ios/legacyDataMigrationTests/Info.plist | 24 + .../legacyDataMigrationTests.m | 66 + .../legacyDataMigration/jest.config.js | 3 + .../legacyDataMigration/metro.config.js | 11 + .../legacyDataMigration/package.json | 39 + .../legacyDataMigration/tsconfig.json | 3 + .../legacyDataMigration/yarn.lock | 6481 +++++++++++++++++ packages/analytics-core/src/index.ts | 2 +- packages/analytics-core/test/index.test.ts | 2 + .../reactnative/AmplitudeReactNativeModule.kt | 101 + .../reactnative/LegacyDatabaseStorage.kt | 313 + .../ios/AmplitudeReactNative.m | 3 + .../ios/AmplitudeReactNative.swift | 74 + .../project.pbxproj | 26 +- .../ios/LegacyDatabaseStorage.swift | 224 + packages/analytics-react-native/src/config.ts | 33 +- .../src/migration/remnant-data-migration.ts | 168 + .../src/react-native-client.ts | 4 +- .../migration/remnant-data-migration.test.ts | 240 + .../test/mock/setup-mobile.ts | 4 + .../test/react-native-client.test.ts | 6 +- .../src/config/react-native.ts | 1 + 85 files changed, 12400 insertions(+), 30 deletions(-) create mode 100644 examples/react-native/legacyDataMigration/.bundle/config create mode 100644 examples/react-native/legacyDataMigration/.eslintrc.js create mode 100644 examples/react-native/legacyDataMigration/.gitignore create mode 100644 examples/react-native/legacyDataMigration/.prettierrc.js create mode 100644 examples/react-native/legacyDataMigration/.watchmanconfig create mode 100644 examples/react-native/legacyDataMigration/App.tsx create mode 100644 examples/react-native/legacyDataMigration/Gemfile create mode 100644 examples/react-native/legacyDataMigration/MigrationChecker.ts create mode 100644 examples/react-native/legacyDataMigration/README.md create mode 100644 examples/react-native/legacyDataMigration/__tests__/App.test.tsx create mode 100644 examples/react-native/legacyDataMigration/android/app/build.gradle create mode 100644 examples/react-native/legacyDataMigration/android/app/debug.keystore create mode 100644 examples/react-native/legacyDataMigration/android/app/proguard-rules.pro create mode 100644 examples/react-native/legacyDataMigration/android/app/src/debug/AndroidManifest.xml create mode 100644 examples/react-native/legacyDataMigration/android/app/src/debug/java/com/legacydatamigration/ReactNativeFlipper.java create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/AndroidManifest.xml create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/java/com/legacydatamigration/LegacyV3.kt create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/java/com/legacydatamigration/LegacyV4.kt create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/java/com/legacydatamigration/MainActivity.kt create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/java/com/legacydatamigration/MainApplication.kt create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/java/com/legacydatamigration/MainPackage.kt create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/java/com/legacydatamigration/MigrationModule.kt create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/res/drawable/rn_edit_text_material.xml create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/res/values/strings.xml create mode 100644 examples/react-native/legacyDataMigration/android/app/src/main/res/values/styles.xml create mode 100644 examples/react-native/legacyDataMigration/android/app/src/release/java/com/legacydatamigration/ReactNativeFlipper.java create mode 100644 examples/react-native/legacyDataMigration/android/build.gradle create mode 100644 examples/react-native/legacyDataMigration/android/gradle.properties create mode 100644 examples/react-native/legacyDataMigration/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/react-native/legacyDataMigration/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/react-native/legacyDataMigration/android/gradlew create mode 100644 examples/react-native/legacyDataMigration/android/gradlew.bat create mode 100644 examples/react-native/legacyDataMigration/android/settings.gradle create mode 100644 examples/react-native/legacyDataMigration/app.json create mode 100644 examples/react-native/legacyDataMigration/babel.config.js create mode 100644 examples/react-native/legacyDataMigration/index.js create mode 100644 examples/react-native/legacyDataMigration/ios/.xcode.env create mode 100644 examples/react-native/legacyDataMigration/ios/LegacyV3.swift create mode 100644 examples/react-native/legacyDataMigration/ios/LegacyV4.swift create mode 100644 examples/react-native/legacyDataMigration/ios/MigrationModule.m create mode 100644 examples/react-native/legacyDataMigration/ios/MigrationModule.swift create mode 100644 examples/react-native/legacyDataMigration/ios/Podfile create mode 100644 examples/react-native/legacyDataMigration/ios/Podfile.lock create mode 100644 examples/react-native/legacyDataMigration/ios/legacyDataMigration-Bridging-Header.h create mode 100644 examples/react-native/legacyDataMigration/ios/legacyDataMigration.xcodeproj/project.pbxproj create mode 100644 examples/react-native/legacyDataMigration/ios/legacyDataMigration.xcodeproj/xcshareddata/xcschemes/legacyDataMigration.xcscheme create mode 100644 examples/react-native/legacyDataMigration/ios/legacyDataMigration/AppDelegate.h create mode 100644 examples/react-native/legacyDataMigration/ios/legacyDataMigration/AppDelegate.mm create mode 100644 examples/react-native/legacyDataMigration/ios/legacyDataMigration/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 examples/react-native/legacyDataMigration/ios/legacyDataMigration/Images.xcassets/Contents.json create mode 100644 examples/react-native/legacyDataMigration/ios/legacyDataMigration/Info.plist create mode 100644 examples/react-native/legacyDataMigration/ios/legacyDataMigration/LaunchScreen.storyboard create mode 100644 examples/react-native/legacyDataMigration/ios/legacyDataMigration/main.m create mode 100644 examples/react-native/legacyDataMigration/ios/legacyDataMigrationTests/Info.plist create mode 100644 examples/react-native/legacyDataMigration/ios/legacyDataMigrationTests/legacyDataMigrationTests.m create mode 100644 examples/react-native/legacyDataMigration/jest.config.js create mode 100644 examples/react-native/legacyDataMigration/metro.config.js create mode 100644 examples/react-native/legacyDataMigration/package.json create mode 100644 examples/react-native/legacyDataMigration/tsconfig.json create mode 100644 examples/react-native/legacyDataMigration/yarn.lock create mode 100644 packages/analytics-react-native/android/src/main/java/com/amplitude/reactnative/LegacyDatabaseStorage.kt create mode 100644 packages/analytics-react-native/ios/LegacyDatabaseStorage.swift create mode 100644 packages/analytics-react-native/src/migration/remnant-data-migration.ts create mode 100644 packages/analytics-react-native/test/migration/remnant-data-migration.test.ts diff --git a/examples/react-native/legacyDataMigration/.bundle/config b/examples/react-native/legacyDataMigration/.bundle/config new file mode 100644 index 000000000..848943bb5 --- /dev/null +++ b/examples/react-native/legacyDataMigration/.bundle/config @@ -0,0 +1,2 @@ +BUNDLE_PATH: "vendor/bundle" +BUNDLE_FORCE_RUBY_PLATFORM: 1 diff --git a/examples/react-native/legacyDataMigration/.eslintrc.js b/examples/react-native/legacyDataMigration/.eslintrc.js new file mode 100644 index 000000000..187894b6a --- /dev/null +++ b/examples/react-native/legacyDataMigration/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: '@react-native', +}; diff --git a/examples/react-native/legacyDataMigration/.gitignore b/examples/react-native/legacyDataMigration/.gitignore new file mode 100644 index 000000000..0cab2ac6f --- /dev/null +++ b/examples/react-native/legacyDataMigration/.gitignore @@ -0,0 +1,66 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +ios/.xcode.env.local + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ +*.keystore +!debug.keystore + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +**/fastlane/report.xml +**/fastlane/Preview.html +**/fastlane/screenshots +**/fastlane/test_output + +# Bundle artifact +*.jsbundle + +# Ruby / CocoaPods +/ios/Pods/ +/vendor/bundle/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + +# testing +/coverage diff --git a/examples/react-native/legacyDataMigration/.prettierrc.js b/examples/react-native/legacyDataMigration/.prettierrc.js new file mode 100644 index 000000000..2b540746a --- /dev/null +++ b/examples/react-native/legacyDataMigration/.prettierrc.js @@ -0,0 +1,7 @@ +module.exports = { + arrowParens: 'avoid', + bracketSameLine: true, + bracketSpacing: false, + singleQuote: true, + trailingComma: 'all', +}; diff --git a/examples/react-native/legacyDataMigration/.watchmanconfig b/examples/react-native/legacyDataMigration/.watchmanconfig new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/examples/react-native/legacyDataMigration/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/examples/react-native/legacyDataMigration/App.tsx b/examples/react-native/legacyDataMigration/App.tsx new file mode 100644 index 000000000..269e6842c --- /dev/null +++ b/examples/react-native/legacyDataMigration/App.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import {Alert, NativeModules} from 'react-native'; +import {Button, SafeAreaView, StyleSheet} from 'react-native'; +import {createInstance} from '@amplitude/analytics-react-native'; +import {getCookieName} from '@amplitude/analytics-client-common'; +import {UUID, MemoryStorage, STORAGE_PREFIX} from '@amplitude/analytics-core'; +import {Event, UserSession} from '@amplitude/analytics-types'; +import MigrationChecker from './MigrationChecker'; + +interface MigrationModule { + prepareLegacyDatabase( + instanceName: string | undefined, + version: string, + ): Promise; +} + +const migrationModule = NativeModules.MigrationModule as MigrationModule; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + alignItems: 'center', + justifyContent: 'center', + gap: 10, + }, +}); + +function App(): JSX.Element { + return ( + + ` + - `Link` +- The above `tagAllowlist` will only allow `button` and `a` tags to be tracked. + +Note `ingestionMetadata` is for internal use only, you don't need to provide it. + +#### Options + +|Name|Type|Default|Description| +|-|-|-|-| +|`cssSelectorAllowlist`|`string[]`|`undefined`| When provided, only allow elements matching any selector to be tracked. | +|`tagAllowlist`|`string[]`|`['a', 'button', 'input', 'select', 'textarea', 'label']`| Only allow elements with tag in this list to be tracked. | + +### 3. Install plugin to Amplitude SDK + +```typescript +amplitude.add(plugin); +``` + +### 4. Initialize Amplitude SDK + +```typescript +amplitude.init('API_KEY'); +``` diff --git a/packages/plugin-auto-tracking-browser/jest.config.js b/packages/plugin-auto-tracking-browser/jest.config.js new file mode 100644 index 000000000..a0ef09957 --- /dev/null +++ b/packages/plugin-auto-tracking-browser/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../jest.config.js'); +const package = require('./package'); + +module.exports = { + ...baseConfig, + displayName: package.name, + rootDir: '.', + testEnvironment: 'jsdom', + coveragePathIgnorePatterns: ['index.ts'], +}; \ No newline at end of file diff --git a/packages/plugin-auto-tracking-browser/package.json b/packages/plugin-auto-tracking-browser/package.json new file mode 100644 index 000000000..e753cb7d5 --- /dev/null +++ b/packages/plugin-auto-tracking-browser/package.json @@ -0,0 +1,57 @@ +{ + "name": "@amplitude/plugin-auto-tracking-browser", + "version": "0.0.0", + "description": "", + "author": "Amplitude Inc", + "homepage": "https://github.com/amplitude/Amplitude-TypeScript", + "license": "MIT", + "main": "lib/cjs/index.js", + "module": "lib/esm/index.js", + "types": "lib/esm/index.d.ts", + "sideEffects": false, + "publishConfig": { + "access": "public", + "tag": "latest" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/amplitude/Amplitude-TypeScript.git" + }, + "scripts": { + "build": "yarn bundle && yarn build:es5 && yarn build:esm", + "bundle": "rollup --config rollup.config.js", + "build:es5": "tsc -p ./tsconfig.es5.json", + "build:esm": "tsc -p ./tsconfig.esm.json", + "clean": "rimraf node_modules lib coverage", + "fix": "yarn fix:eslint & yarn fix:prettier", + "fix:eslint": "eslint '{src,test}/**/*.ts' --fix", + "fix:prettier": "prettier --write \"{src,test}/**/*.ts\"", + "lint": "yarn lint:eslint & yarn lint:prettier", + "lint:eslint": "eslint '{src,test}/**/*.ts'", + "lint:prettier": "prettier --check \"{src,test}/**/*.ts\"", + "publish": "node ../../scripts/publish/upload-to-s3.js", + "test": "jest", + "typecheck": "tsc -p ./tsconfig.json" + }, + "bugs": { + "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" + }, + "dependencies": { + "@amplitude/analytics-client-common": ">=1 <3", + "@amplitude/analytics-types": ">=1 <3", + "tslib": "^2.4.1" + }, + "devDependencies": { + "@amplitude/analytics-browser": "^2.1.2", + "@rollup/plugin-commonjs": "^23.0.4", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^10.0.1", + "rollup": "^2.79.1", + "rollup-plugin-execute": "^1.1.1", + "rollup-plugin-gzip": "^3.1.0", + "rollup-plugin-terser": "^7.0.2" + }, + "files": [ + "lib" + ] +} diff --git a/packages/plugin-auto-tracking-browser/rollup.config.js b/packages/plugin-auto-tracking-browser/rollup.config.js new file mode 100644 index 000000000..0ea990975 --- /dev/null +++ b/packages/plugin-auto-tracking-browser/rollup.config.js @@ -0,0 +1,6 @@ +import { iife, umd } from '../../scripts/build/rollup.config'; + +iife.input = umd.input; +iife.output.name = 'amplitudeAutoTrackingPlugin'; + +export default [umd, iife]; diff --git a/packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts b/packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts new file mode 100644 index 000000000..b4ad47def --- /dev/null +++ b/packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts @@ -0,0 +1,191 @@ +/* eslint-disable no-restricted-globals */ +import { BrowserClient, BrowserConfig, EnrichmentPlugin, IngestionMetadata } from '@amplitude/analytics-types'; +import * as constants from './constants'; +import { getText } from './helpers'; + +type BrowserEnrichmentPlugin = EnrichmentPlugin; +type ActionType = 'click' | 'change'; + +const DEFAULT_TAG_ALLOWLIST = ['a', 'button', 'input', 'select', 'textarea', 'label']; + +interface EventListener { + element: Element; + type: ActionType; + handler: () => void; +} + +interface Options { + cssSelectorAllowlist?: string[]; + tagAllowlist?: string[]; + ingestionMetadata?: IngestionMetadata; // Should avoid overriding this if unplanned to do so, this is not available in the charts. +} + +export const autoTrackingPlugin = (options: Options = {}): BrowserEnrichmentPlugin => { + const { tagAllowlist = DEFAULT_TAG_ALLOWLIST, cssSelectorAllowlist, ingestionMetadata } = options; + const name = constants.PLUGIN_NAME; + const type = 'enrichment'; + + let observer: MutationObserver | undefined; + let eventListeners: EventListener[] = []; + + const addEventListener = (element: Element, type: ActionType, handler: () => void) => { + element.addEventListener(type, handler); + eventListeners.push({ + element: element, + type: type, + handler: handler, + }); + }; + + const removeEventListeners = () => { + eventListeners.forEach((_ref) => { + const element = _ref.element, + type = _ref.type, + handler = _ref.handler; + /* istanbul ignore next */ + element?.removeEventListener(type, handler); + }); + eventListeners = []; + }; + + const shouldTrackEvent = (actionType: ActionType, element: Element) => { + /* istanbul ignore if */ + if (!element) { + return false; + } + /* istanbul ignore next */ + const elementType = (element as HTMLInputElement)?.type || ''; + if (typeof elementType === 'string') { + switch (elementType.toLowerCase()) { + case 'hidden': + return false; + case 'password': + return false; + } + } + const tag = element.tagName.toLowerCase(); + /* istanbul ignore if */ + if (!tagAllowlist.includes(tag)) { + return false; + } + if (cssSelectorAllowlist) { + const hasMatchAnyAllowedSelector = cssSelectorAllowlist.some((selector) => element.matches(selector)); + if (!hasMatchAnyAllowedSelector) { + return false; + } + } + switch (tag) { + case 'input': + case 'select': + case 'textarea': + return actionType === 'change' || actionType === 'click'; + default: { + /* istanbul ignore next */ + const computedStyle = window?.getComputedStyle?.(element); + /* istanbul ignore next */ + if (computedStyle && computedStyle.getPropertyValue('cursor') === 'pointer' && actionType === 'click') { + return true; + } + return actionType === 'click'; + } + } + }; + + const getEventProperties = (actionType: ActionType, element: Element) => { + const tag = element.tagName.toLowerCase(); + /* istanbul ignore next */ + const rect = + typeof element.getBoundingClientRect === 'function' ? element.getBoundingClientRect() : { left: null, top: null }; + /* istanbul ignore next */ + const properties: Record = { + [constants.AMPLITUDE_EVENT_PROP_ELEMENT_ID]: element.id, + [constants.AMPLITUDE_EVENT_PROP_ELEMENT_CLASS]: element.className, + [constants.AMPLITUDE_EVENT_PROP_ELEMENT_TAG]: tag, + [constants.AMPLITUDE_EVENT_PROP_ELEMENT_TEXT]: getText(element), + [constants.AMPLITUDE_EVENT_PROP_ELEMENT_POSITION_LEFT]: rect.left == null ? null : Math.round(rect.left), + [constants.AMPLITUDE_EVENT_PROP_ELEMENT_POSITION_TOP]: rect.top == null ? null : Math.round(rect.top), + [constants.AMPLITUDE_EVENT_PROP_PAGE_URL]: window.location.href.split('?')[0], + [constants.AMPLITUDE_EVENT_PROP_PAGE_TITLE]: (typeof document !== 'undefined' && document.title) || '', + [constants.AMPLITUDE_EVENT_PROP_VIEWPORT_HEIGHT]: window.innerHeight, + [constants.AMPLITUDE_EVENT_PROP_VIEWPORT_WIDTH]: window.innerWidth, + }; + if (tag === 'a' && actionType === 'click' && element instanceof HTMLAnchorElement) { + properties[constants.AMPLITUDE_EVENT_PROP_ELEMENT_HREF] = element.href; + } + return properties; + }; + + const setup: BrowserEnrichmentPlugin['setup'] = async (config, amplitude) => { + if (!amplitude) { + /* istanbul ignore next */ + config?.loggerProvider?.warn( + `${name} plugin requires a later version of @amplitude/analytics-browser. Events are not tracked.`, + ); + return; + } + /* istanbul ignore if */ + if (typeof document === 'undefined') { + return; + } + const addListener = (el: Element) => { + if (shouldTrackEvent('click', el)) { + addEventListener(el, 'click', () => { + /* istanbul ignore next */ + amplitude?.track(constants.AMPLITUDE_ELEMENT_CLICKED_EVENT, getEventProperties('click', el)); + }); + } + if (shouldTrackEvent('change', el)) { + addEventListener(el, 'change', () => { + /* istanbul ignore next */ + amplitude?.track(constants.AMPLITUDE_ELEMENT_CHANGED_EVENT, getEventProperties('change', el)); + }); + } + }; + const allElements = Array.from(document.body.querySelectorAll(tagAllowlist.join(','))); + allElements.forEach(addListener); + if (typeof MutationObserver !== 'undefined') { + observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node: Node) => { + addListener(node as Element); + if ('querySelectorAll' in node && typeof node.querySelectorAll === 'function') { + Array.from(node.querySelectorAll(tagAllowlist.join(',')) as HTMLElement[]).map(addListener); + } + }); + }); + }); + observer.observe(document.body, { + subtree: true, + childList: true, + }); + } + /* istanbul ignore next */ + config?.loggerProvider?.log(`${name} has been successfully added.`); + }; + + const execute: BrowserEnrichmentPlugin['execute'] = async (event) => { + const { sourceName, sourceVersion } = ingestionMetadata || {}; + if (sourceName && sourceVersion) { + event.ingestion_metadata = { + source_name: `${constants.INGESTION_METADATA_SOURCE_NAME_PREFIX}${sourceName}`, // Make sure the source name is prefixed with the correct context. + source_version: sourceVersion, + }; + } + return event; + }; + + const teardown = async () => { + if (observer) { + observer.disconnect(); + } + removeEventListeners(); + }; + + return { + name, + type, + setup, + execute, + teardown, + }; +}; diff --git a/packages/plugin-auto-tracking-browser/src/constants.ts b/packages/plugin-auto-tracking-browser/src/constants.ts new file mode 100644 index 000000000..9644d0194 --- /dev/null +++ b/packages/plugin-auto-tracking-browser/src/constants.ts @@ -0,0 +1,18 @@ +export const PLUGIN_NAME = '@amplitude/plugin-auto-tracking-browser'; + +export const AMPLITUDE_ELEMENT_CLICKED_EVENT = '[Amplitude] Element Clicked'; +export const AMPLITUDE_ELEMENT_CHANGED_EVENT = '[Amplitude] Element Changed'; + +export const AMPLITUDE_EVENT_PROP_ELEMENT_ID = '[Amplitude] Element ID'; +export const AMPLITUDE_EVENT_PROP_ELEMENT_CLASS = '[Amplitude] Element Class'; +export const AMPLITUDE_EVENT_PROP_ELEMENT_TAG = '[Amplitude] Element Tag'; +export const AMPLITUDE_EVENT_PROP_ELEMENT_TEXT = '[Amplitude] Element Text'; +export const AMPLITUDE_EVENT_PROP_ELEMENT_HREF = '[Amplitude] Element Href'; +export const AMPLITUDE_EVENT_PROP_ELEMENT_POSITION_LEFT = '[Amplitude] Element Position Left'; +export const AMPLITUDE_EVENT_PROP_ELEMENT_POSITION_TOP = '[Amplitude] Element Position Top'; +export const AMPLITUDE_EVENT_PROP_PAGE_URL = '[Amplitude] Page URL'; +export const AMPLITUDE_EVENT_PROP_PAGE_TITLE = '[Amplitude] Page Title'; +export const AMPLITUDE_EVENT_PROP_VIEWPORT_HEIGHT = '[Amplitude] Viewport Height'; +export const AMPLITUDE_EVENT_PROP_VIEWPORT_WIDTH = '[Amplitude] Viewport Width'; + +export const INGESTION_METADATA_SOURCE_NAME_PREFIX = 'browser-typescript-'; diff --git a/packages/plugin-auto-tracking-browser/src/helpers.ts b/packages/plugin-auto-tracking-browser/src/helpers.ts new file mode 100644 index 000000000..738d17c48 --- /dev/null +++ b/packages/plugin-auto-tracking-browser/src/helpers.ts @@ -0,0 +1,53 @@ +const SENTITIVE_TAGS = ['input', 'select', 'textarea']; + +export const isNonSensitiveString = (text: string | null) => { + if (text == null) { + return false; + } + if (typeof text === 'string') { + const ccRegex = + /^(?:(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})|(6(?:011|5[0-9]{2})[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35[0-9]{3})[0-9]{11}))$/; + if (ccRegex.test((text || '').replace(/[- ]/g, ''))) { + return false; + } + const ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/; + if (ssnRegex.test(text)) { + return false; + } + } + return true; +}; + +export const isTextNode = (node: Node) => { + return !!node && node.nodeType === 3; +}; + +export const isNonSensitiveElement = (element: Element) => { + const tag = element.tagName.toLowerCase(); + return !SENTITIVE_TAGS.includes(tag); +}; + +// Maybe this can be simplified with element.innerText, keep and manual concatenating for now, more research needed. +export const getText = (element: Element): string => { + let text = ''; + if (isNonSensitiveElement(element) && element.childNodes && element.childNodes.length) { + element.childNodes.forEach((child) => { + let childText = ''; + if (isTextNode(child)) { + if (child.textContent) { + childText = child.textContent; + } + } else { + childText = getText(child as Element); + } + text += childText + .split(/(\s+)/) + .filter(isNonSensitiveString) + .join('') + .replace(/[\r\n]/g, ' ') + .replace(/[ ]+/g, ' ') + .substring(0, 255); + }); + } + return text; +}; diff --git a/packages/plugin-auto-tracking-browser/src/index.ts b/packages/plugin-auto-tracking-browser/src/index.ts new file mode 100644 index 000000000..8a184b4f1 --- /dev/null +++ b/packages/plugin-auto-tracking-browser/src/index.ts @@ -0,0 +1 @@ +export { autoTrackingPlugin as plugin, autoTrackingPlugin } from './auto-tracking-plugin'; diff --git a/packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts b/packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts new file mode 100644 index 000000000..f6ee22758 --- /dev/null +++ b/packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts @@ -0,0 +1,418 @@ +import { autoTrackingPlugin } from '../src/auto-tracking-plugin'; +import { BrowserClient, BrowserConfig, EnrichmentPlugin, Logger } from '@amplitude/analytics-types'; +import { createInstance } from '@amplitude/analytics-browser'; + +const mockWindowLocationFromURL = (url: URL) => { + window.location.href = url.toString(); + window.location.search = url.search; + window.location.hostname = url.hostname; + window.location.pathname = url.pathname; +}; + +describe('autoTrackingPlugin', () => { + let plugin: EnrichmentPlugin | undefined; + + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + hostname: '', + href: '', + pathname: '', + search: '', + }, + writable: true, + }); + }); + + beforeEach(() => { + (window.location as any) = { + hostname: '', + href: '', + pathname: '', + search: '', + }; + plugin = autoTrackingPlugin(); + }); + + afterEach(() => { + void plugin?.teardown?.(); + jest.clearAllMocks(); + }); + + describe('name', () => { + test('should return the plugin name', () => { + expect(plugin?.name).toBe('@amplitude/plugin-auto-tracking-browser'); + }); + }); + + describe('type', () => { + test('should return the plugin type', () => { + expect(plugin?.type).toBe('enrichment'); + }); + }); + + describe('setup', () => { + test('should setup successfully', async () => { + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + const amplitude: Partial = {}; + await plugin?.setup?.(config as BrowserConfig, amplitude as BrowserClient); + expect(loggerProvider.warn).toHaveBeenCalledTimes(0); + expect(loggerProvider.log).toHaveBeenCalledTimes(1); + expect(loggerProvider.log).toHaveBeenNthCalledWith(1, `${plugin?.name as string} has been successfully added.`); + }); + + test('should handle incompatible Amplitude SDK version', async () => { + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup?.(config as BrowserConfig); + expect(loggerProvider.warn).toHaveBeenCalledTimes(1); + expect(loggerProvider.warn).toHaveBeenNthCalledWith( + 1, + `${ + plugin?.name as string + } plugin requires a later version of @amplitude/analytics-browser. Events are not tracked.`, + ); + }); + }); + + describe('execute', () => { + test('should return the same event type', async () => { + const event = await plugin?.execute({ + event_type: 'custom_event', + }); + expect(event).toEqual({ + event_type: 'custom_event', + }); + }); + + test('should enrich ingestion_metadata field', async () => { + plugin = autoTrackingPlugin({ + ingestionMetadata: { + sourceName: 'unit-test', + sourceVersion: '1.0.0', + }, + }); + const event = await plugin?.execute({ + event_type: 'custom_event', + }); + expect(event).toEqual({ + event_type: 'custom_event', + ingestion_metadata: { + source_name: 'browser-typescript-unit-test', + source_version: '1.0.0', + }, + }); + }); + + test('should not enrich ingestion_metadata field if source_name is missing', async () => { + plugin = autoTrackingPlugin({ + ingestionMetadata: { + sourceVersion: '1.0.0', + }, + }); + const event = await plugin?.execute({ + event_type: 'custom_event', + }); + expect(event).toEqual({ + event_type: 'custom_event', + }); + }); + + test('should not enrich ingestion_metadata field if source_version is missing', async () => { + plugin = autoTrackingPlugin({ + ingestionMetadata: { + sourceName: 'unit-test', + }, + }); + const event = await plugin?.execute({ + event_type: 'custom_event', + }); + expect(event).toEqual({ + event_type: 'custom_event', + }); + }); + }); + + describe('auto-tracking events', () => { + const API_KEY = 'API_KEY'; + const USER_ID = 'USER_ID'; + + let instance = createInstance(); + let track: jest.SpyInstance; + + beforeEach(async () => { + plugin = autoTrackingPlugin(); + instance = createInstance(); + await instance.init(API_KEY, USER_ID).promise; + track = jest.spyOn(instance, 'track'); + + const link = document.createElement('a'); + link.setAttribute('id', 'my-link-id'); + link.setAttribute('class', 'my-link-class'); + link.href = 'https://www.amplitude.com/click-link'; + link.text = 'my-link-text'; + document.body.appendChild(link); + + mockWindowLocationFromURL(new URL('https://www.amplitude.com/unit-test?query=param')); + }); + + afterEach(() => { + document.querySelector('a#my-link-id')?.remove(); + document.querySelector('button#my-button-id')?.remove(); + document.querySelector('input#my-input-id')?.remove(); + }); + + test('should monitor element clicked event', async () => { + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click event + document.getElementById('my-link-id')?.dispatchEvent(new Event('click')); + + expect(track).toHaveBeenCalledTimes(1); + expect(track).toHaveBeenNthCalledWith(1, '[Amplitude] Element Clicked', { + '[Amplitude] Element Class': 'my-link-class', + '[Amplitude] Element Href': 'https://www.amplitude.com/click-link', + '[Amplitude] Element ID': 'my-link-id', + '[Amplitude] Element Position Left': 0, + '[Amplitude] Element Position Top': 0, + '[Amplitude] Element Tag': 'a', + '[Amplitude] Element Text': 'my-link-text', + '[Amplitude] Page Title': '', + '[Amplitude] Page URL': 'https://www.amplitude.com/unit-test', + '[Amplitude] Viewport Height': 768, + '[Amplitude] Viewport Width': 1024, + }); + + // stop observer and listeners + await plugin?.teardown?.(); + + // trigger click event + document.getElementById('my-link-id')?.dispatchEvent(new Event('click')); + + // assert no additional event was tracked + expect(track).toHaveBeenCalledTimes(1); + }); + + test('should monitor element clicked event when dynamically rendered', async () => { + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click event + const button = document.createElement('button'); + const buttonText = document.createTextNode('submit'); + button.setAttribute('id', 'my-button-id'); + button.setAttribute('class', 'my-button-class'); + button.appendChild(buttonText); + document.body.appendChild(button); + // allow mutation observer to execute and event listener to be attached + await new Promise((r) => r(undefined)); // basically, await next clock tick + document.getElementById('my-button-id')?.dispatchEvent(new Event('click')); + + expect(track).toHaveBeenCalledTimes(1); + expect(track).toHaveBeenNthCalledWith(1, '[Amplitude] Element Clicked', { + '[Amplitude] Element Class': 'my-button-class', + '[Amplitude] Element ID': 'my-button-id', + '[Amplitude] Element Position Left': 0, + '[Amplitude] Element Position Top': 0, + '[Amplitude] Element Tag': 'button', + '[Amplitude] Element Text': 'submit', + '[Amplitude] Page Title': '', + '[Amplitude] Page URL': 'https://www.amplitude.com/unit-test', + '[Amplitude] Viewport Height': 768, + '[Amplitude] Viewport Width': 1024, + }); + + // stop observer and listeners + await plugin?.teardown?.(); + + // trigger click event + document.getElementById('my-link-id')?.dispatchEvent(new Event('click')); + + // assert no additional event was tracked + expect(track).toHaveBeenCalledTimes(1); + }); + + test('should follow tagAllowlist configuration', async () => { + plugin = autoTrackingPlugin({ tagAllowlist: ['button'] }); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click event + document.getElementById('my-link-id')?.dispatchEvent(new Event('click')); + + expect(track).toHaveBeenCalledTimes(0); + }); + + test('should follow cssSelectorAllowlist configuration', async () => { + const button = document.createElement('button'); + const buttonText = document.createTextNode('submit'); + button.setAttribute('id', 'my-button-id'); + button.setAttribute('class', 'my-button-class'); + button.appendChild(buttonText); + document.body.appendChild(button); + + plugin = autoTrackingPlugin({ cssSelectorAllowlist: ['.my-button-class'] }); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click link + document.getElementById('my-link-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(0); + + // trigger click button + document.getElementById('my-button-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(1); + }); + + test('should track change event', async () => { + const input = document.createElement('input'); + input.setAttribute('id', 'my-input-id'); + input.setAttribute('class', 'my-input-class'); + document.body.appendChild(input); + + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click input + document.getElementById('my-input-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(1); + + // trigger change input + document.getElementById('my-input-id')?.dispatchEvent(new Event('change')); + expect(track).toHaveBeenCalledTimes(2); + }); + + test('should not track when element type is hidden', async () => { + const input = document.createElement('input'); + input.setAttribute('id', 'my-input-id'); + input.setAttribute('class', 'my-input-class'); + input.type = 'hidden'; + document.body.appendChild(input); + + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click input + document.getElementById('my-input-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(0); + + // trigger change input + document.getElementById('my-input-id')?.dispatchEvent(new Event('change')); + expect(track).toHaveBeenCalledTimes(0); + }); + + test('should not track when element type is password', async () => { + const input = document.createElement('input'); + input.setAttribute('id', 'my-input-id'); + input.setAttribute('class', 'my-input-class'); + input.type = 'password'; + document.body.appendChild(input); + + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click input + document.getElementById('my-input-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(0); + + // trigger change input + document.getElementById('my-input-id')?.dispatchEvent(new Event('change')); + expect(track).toHaveBeenCalledTimes(0); + }); + + test('should not track for div tags', async () => { + const div = document.createElement('div'); + div.setAttribute('id', 'my-div-id'); + div.setAttribute('class', 'my-div-class'); + document.body.appendChild(div); + + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click input + document.getElementById('my-div-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(0); + + // trigger change input + document.getElementById('my-div-id')?.dispatchEvent(new Event('change')); + expect(track).toHaveBeenCalledTimes(0); + }); + }); + + describe('teardown', () => { + // eslint-disable-next-line jest/expect-expect + test('should teardown plugin', () => { + void plugin?.teardown?.(); + }); + }); +}); diff --git a/packages/plugin-auto-tracking-browser/test/helpers.test.ts b/packages/plugin-auto-tracking-browser/test/helpers.test.ts new file mode 100644 index 000000000..b8d92c4bf --- /dev/null +++ b/packages/plugin-auto-tracking-browser/test/helpers.test.ts @@ -0,0 +1,126 @@ +import { isNonSensitiveString, isTextNode, isNonSensitiveElement, getText } from '../src/helpers'; + +describe('autoTrackingPlugin helpers', () => { + describe('isNonSensitiveString', () => { + test('should return false when text is missing', () => { + const text = null; + const result = isNonSensitiveString(text); + expect(result).toEqual(false); + }); + + test('should return true when text is not sensitive', () => { + const text = 'test-string'; + const result = isNonSensitiveString(text); + expect(result).toEqual(true); + }); + + test('should return false when text is credit card format', () => { + const text = '4916024123820164'; + const result = isNonSensitiveString(text); + expect(result).toEqual(false); + }); + + test('should return false when text is social security number format', () => { + const text = '269-28-9315'; + const result = isNonSensitiveString(text); + expect(result).toEqual(false); + }); + + test('should return true when text is not a string', () => { + const text = 123; + const result = isNonSensitiveString(text as unknown as string); + expect(result).toEqual(true); + }); + }); + + describe('isTextNode', () => { + test('should return false when node is not a text node', () => { + const node = document.createElement('a'); + const result = isTextNode(node); + expect(result).toEqual(false); + }); + + test('should return false when node is missing', () => { + const node = null; + const result = isTextNode(node as unknown as Node); + expect(result).toEqual(false); + }); + + test('should return true when node is a text node', () => { + const node = document.createTextNode('text'); + const result = isTextNode(node); + expect(result).toEqual(true); + }); + }); + + describe('isNonSensitiveElement', () => { + test('should return false when element is not a sensitive tag', () => { + const element = document.createElement('textarea'); + const result = isNonSensitiveElement(element); + expect(result).toEqual(false); + }); + + test('should return false when element is a sensitive tag', () => { + const element = document.createElement('a'); + const result = isNonSensitiveElement(element); + expect(result).toEqual(true); + }); + }); + + describe('getText', () => { + test('should return empty string when element is sensitive', () => { + const element = document.createElement('input'); + element.value = 'test'; + const result = getText(element); + expect(result).toEqual(''); + }); + + test('should return text when element has text attribute', () => { + const element = document.createElement('a'); + element.text = 'test'; + const result = getText(element); + expect(result).toEqual('test'); + }); + + test('should return text when element has text node', () => { + const button = document.createElement('button'); + const buttonText = document.createTextNode('submit'); + button.appendChild(buttonText); + const result = getText(button); + expect(result).toEqual('submit'); + }); + + test('should return concatenated text when element has child text nodes', () => { + const button = document.createElement('button'); + const buttonText = document.createTextNode('submit'); + button.appendChild(buttonText); + const div = document.createElement('div'); + div.textContent = ' and pay'; + button.appendChild(div); + const result = getText(button); + expect(result).toEqual('submit and pay'); + }); + + test('should return concatenated text with sensitive text filtered', () => { + const button = document.createElement('button'); + const buttonText = document.createTextNode('submit'); + button.appendChild(buttonText); + const div = document.createElement('div'); + div.textContent = '269-28-9315'; + button.appendChild(div); + const result = getText(button); + expect(result).toEqual('submit'); + }); + + test('should return concatenated text with extra space removed', () => { + const button = document.createElement('button'); + const buttonText = document.createTextNode('submit'); + button.appendChild(buttonText); + const div = document.createElement('div'); + div.textContent = ' and \n pay'; + button.appendChild(div); + const result = getText(button); + expect(result).toEqual('submit and pay'); + }); + }); +}); diff --git a/packages/plugin-auto-tracking-browser/tsconfig.es5.json b/packages/plugin-auto-tracking-browser/tsconfig.es5.json new file mode 100644 index 000000000..77e041d3f --- /dev/null +++ b/packages/plugin-auto-tracking-browser/tsconfig.es5.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"], + "compilerOptions": { + "module": "commonjs", + "noEmit": false, + "outDir": "lib/cjs", + "rootDir": "./src" + } +} diff --git a/packages/plugin-auto-tracking-browser/tsconfig.esm.json b/packages/plugin-auto-tracking-browser/tsconfig.esm.json new file mode 100644 index 000000000..bec981eee --- /dev/null +++ b/packages/plugin-auto-tracking-browser/tsconfig.esm.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"], + "compilerOptions": { + "module": "es6", + "noEmit": false, + "outDir": "lib/esm", + "rootDir": "./src" + } +} diff --git a/packages/plugin-auto-tracking-browser/tsconfig.json b/packages/plugin-auto-tracking-browser/tsconfig.json new file mode 100644 index 000000000..68713e643 --- /dev/null +++ b/packages/plugin-auto-tracking-browser/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*", "test/**/*"], + "compilerOptions": { + "baseUrl": ".", + "esModuleInterop": true, + "lib": ["dom"], + "noEmit": true, + "rootDir": "." + } +} diff --git a/packages/plugin-ga-events-forwarder-browser/test/ga-events-forwarder.test.ts b/packages/plugin-ga-events-forwarder-browser/test/ga-events-forwarder.test.ts index 109bdcd5e..642c2a03c 100644 --- a/packages/plugin-ga-events-forwarder-browser/test/ga-events-forwarder.test.ts +++ b/packages/plugin-ga-events-forwarder-browser/test/ga-events-forwarder.test.ts @@ -114,7 +114,7 @@ describe('gaEventsForwarderPlugin', () => { }); describe('execute', () => { - test('should return the return the same event type', async () => { + test('should return the same event type', async () => { const event = await plugin?.execute({ event_type: 'custom_event', }); From 101a186c80e6fc3d506efde40dc71d64d6d930dc Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 31 Aug 2023 17:38:16 +0000 Subject: [PATCH 147/214] chore(release): publish - @amplitude/plugin-auto-tracking-browser@0.1.0 - @amplitude/plugin-ga-events-forwarder-browser@0.3.0 --- packages/plugin-auto-tracking-browser/CHANGELOG.md | 12 ++++++++++++ packages/plugin-auto-tracking-browser/package.json | 2 +- .../plugin-ga-events-forwarder-browser/CHANGELOG.md | 12 ++++++++++++ .../plugin-ga-events-forwarder-browser/package.json | 2 +- .../src/version.ts | 2 +- 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/plugin-auto-tracking-browser/CHANGELOG.md b/packages/plugin-auto-tracking-browser/CHANGELOG.md index 1f6f37127..4fd6ecc16 100644 --- a/packages/plugin-auto-tracking-browser/CHANGELOG.md +++ b/packages/plugin-auto-tracking-browser/CHANGELOG.md @@ -2,3 +2,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.1.0 (2023-08-31) + +### Features + +- add auto-tracking plugin ([#570](https://github.com/amplitude/Amplitude-TypeScript/issues/570)) + ([757032f](https://github.com/amplitude/Amplitude-TypeScript/commit/757032f5c0eeac4396f28163ad14958ea44e8ace)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/packages/plugin-auto-tracking-browser/package.json b/packages/plugin-auto-tracking-browser/package.json index e753cb7d5..ddc1c22da 100644 --- a/packages/plugin-auto-tracking-browser/package.json +++ b/packages/plugin-auto-tracking-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-auto-tracking-browser", - "version": "0.0.0", + "version": "0.1.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", diff --git a/packages/plugin-ga-events-forwarder-browser/CHANGELOG.md b/packages/plugin-ga-events-forwarder-browser/CHANGELOG.md index ed9e296cd..d5ac38bbe 100644 --- a/packages/plugin-ga-events-forwarder-browser/CHANGELOG.md +++ b/packages/plugin-ga-events-forwarder-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.3.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-ga-events-forwarder-browser@0.2.0...@amplitude/plugin-ga-events-forwarder-browser@0.3.0) (2023-08-31) + +### Features + +- add auto-tracking plugin ([#570](https://github.com/amplitude/Amplitude-TypeScript/issues/570)) + ([757032f](https://github.com/amplitude/Amplitude-TypeScript/commit/757032f5c0eeac4396f28163ad14958ea44e8ace)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [0.2.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-ga-events-forwarder-browser@0.1.2...@amplitude/plugin-ga-events-forwarder-browser@0.2.0) (2023-08-25) ### Bug Fixes diff --git a/packages/plugin-ga-events-forwarder-browser/package.json b/packages/plugin-ga-events-forwarder-browser/package.json index 04f707466..c7fbb7fd5 100644 --- a/packages/plugin-ga-events-forwarder-browser/package.json +++ b/packages/plugin-ga-events-forwarder-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-ga-events-forwarder-browser", - "version": "0.2.0", + "version": "0.3.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", diff --git a/packages/plugin-ga-events-forwarder-browser/src/version.ts b/packages/plugin-ga-events-forwarder-browser/src/version.ts index 8e1492f82..ffd80fd3a 100644 --- a/packages/plugin-ga-events-forwarder-browser/src/version.ts +++ b/packages/plugin-ga-events-forwarder-browser/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.2.0'; +export const VERSION = '0.3.0'; From 34647ea93673732014bb2c6d5d4de863e1f46955 Mon Sep 17 00:00:00 2001 From: Marvin Liu Date: Tue, 5 Sep 2023 13:45:14 -0700 Subject: [PATCH 148/214] refactor(plugin-auto-tracking-browser): remove ingestion metadata option (#573) * refactor(plugin-auto-tracking-browser): remove ingestion metadata option * docs(plugin-auto-tracking-browser): add notice --- .../plugin-auto-tracking-browser/README.md | 7 ++- .../src/auto-tracking-plugin.ts | 12 +---- .../src/constants.ts | 2 - .../test/auto-tracking-plugin.test.ts | 47 ------------------- 4 files changed, 5 insertions(+), 63 deletions(-) diff --git a/packages/plugin-auto-tracking-browser/README.md b/packages/plugin-auto-tracking-browser/README.md index 5b2122e70..12838d5c7 100644 --- a/packages/plugin-auto-tracking-browser/README.md +++ b/packages/plugin-auto-tracking-browser/README.md @@ -5,9 +5,10 @@

-# @amplitude/plugin-auto-tracking-browser +# @amplitude/plugin-auto-tracking-browser (alpha) +**This plugin is in alpha stage, naming and interface might change in the future.** -Official Browser SDK plugin for auto-tracking. +Browser SDK plugin for auto-tracking. ## Installation @@ -54,8 +55,6 @@ Examples: - `Link` - The above `tagAllowlist` will only allow `button` and `a` tags to be tracked. -Note `ingestionMetadata` is for internal use only, you don't need to provide it. - #### Options |Name|Type|Default|Description| diff --git a/packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts b/packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts index b4ad47def..b3a12a45c 100644 --- a/packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts +++ b/packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts @@ -1,5 +1,5 @@ /* eslint-disable no-restricted-globals */ -import { BrowserClient, BrowserConfig, EnrichmentPlugin, IngestionMetadata } from '@amplitude/analytics-types'; +import { BrowserClient, BrowserConfig, EnrichmentPlugin } from '@amplitude/analytics-types'; import * as constants from './constants'; import { getText } from './helpers'; @@ -17,11 +17,10 @@ interface EventListener { interface Options { cssSelectorAllowlist?: string[]; tagAllowlist?: string[]; - ingestionMetadata?: IngestionMetadata; // Should avoid overriding this if unplanned to do so, this is not available in the charts. } export const autoTrackingPlugin = (options: Options = {}): BrowserEnrichmentPlugin => { - const { tagAllowlist = DEFAULT_TAG_ALLOWLIST, cssSelectorAllowlist, ingestionMetadata } = options; + const { tagAllowlist = DEFAULT_TAG_ALLOWLIST, cssSelectorAllowlist } = options; const name = constants.PLUGIN_NAME; const type = 'enrichment'; @@ -164,13 +163,6 @@ export const autoTrackingPlugin = (options: Options = {}): BrowserEnrichmentPlug }; const execute: BrowserEnrichmentPlugin['execute'] = async (event) => { - const { sourceName, sourceVersion } = ingestionMetadata || {}; - if (sourceName && sourceVersion) { - event.ingestion_metadata = { - source_name: `${constants.INGESTION_METADATA_SOURCE_NAME_PREFIX}${sourceName}`, // Make sure the source name is prefixed with the correct context. - source_version: sourceVersion, - }; - } return event; }; diff --git a/packages/plugin-auto-tracking-browser/src/constants.ts b/packages/plugin-auto-tracking-browser/src/constants.ts index 9644d0194..755c03193 100644 --- a/packages/plugin-auto-tracking-browser/src/constants.ts +++ b/packages/plugin-auto-tracking-browser/src/constants.ts @@ -14,5 +14,3 @@ export const AMPLITUDE_EVENT_PROP_PAGE_URL = '[Amplitude] Page URL'; export const AMPLITUDE_EVENT_PROP_PAGE_TITLE = '[Amplitude] Page Title'; export const AMPLITUDE_EVENT_PROP_VIEWPORT_HEIGHT = '[Amplitude] Viewport Height'; export const AMPLITUDE_EVENT_PROP_VIEWPORT_WIDTH = '[Amplitude] Viewport Width'; - -export const INGESTION_METADATA_SOURCE_NAME_PREFIX = 'browser-typescript-'; diff --git a/packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts b/packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts index f6ee22758..1cec8f95d 100644 --- a/packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts +++ b/packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts @@ -97,53 +97,6 @@ describe('autoTrackingPlugin', () => { event_type: 'custom_event', }); }); - - test('should enrich ingestion_metadata field', async () => { - plugin = autoTrackingPlugin({ - ingestionMetadata: { - sourceName: 'unit-test', - sourceVersion: '1.0.0', - }, - }); - const event = await plugin?.execute({ - event_type: 'custom_event', - }); - expect(event).toEqual({ - event_type: 'custom_event', - ingestion_metadata: { - source_name: 'browser-typescript-unit-test', - source_version: '1.0.0', - }, - }); - }); - - test('should not enrich ingestion_metadata field if source_name is missing', async () => { - plugin = autoTrackingPlugin({ - ingestionMetadata: { - sourceVersion: '1.0.0', - }, - }); - const event = await plugin?.execute({ - event_type: 'custom_event', - }); - expect(event).toEqual({ - event_type: 'custom_event', - }); - }); - - test('should not enrich ingestion_metadata field if source_version is missing', async () => { - plugin = autoTrackingPlugin({ - ingestionMetadata: { - sourceName: 'unit-test', - }, - }); - const event = await plugin?.execute({ - event_type: 'custom_event', - }); - expect(event).toEqual({ - event_type: 'custom_event', - }); - }); }); describe('auto-tracking events', () => { From 0d18ae44b6d63bf2ea9eec548d335998e63d0023 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Tue, 5 Sep 2023 21:51:18 +0000 Subject: [PATCH 149/214] chore(release): publish - @amplitude/plugin-auto-tracking-browser@0.1.1 --- packages/plugin-auto-tracking-browser/CHANGELOG.md | 9 +++++++++ packages/plugin-auto-tracking-browser/package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/plugin-auto-tracking-browser/CHANGELOG.md b/packages/plugin-auto-tracking-browser/CHANGELOG.md index 4fd6ecc16..6a2c5a072 100644 --- a/packages/plugin-auto-tracking-browser/CHANGELOG.md +++ b/packages/plugin-auto-tracking-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-auto-tracking-browser@0.1.0...@amplitude/plugin-auto-tracking-browser@0.1.1) (2023-09-05) + +**Note:** Version bump only for package @amplitude/plugin-auto-tracking-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # 0.1.0 (2023-08-31) ### Features diff --git a/packages/plugin-auto-tracking-browser/package.json b/packages/plugin-auto-tracking-browser/package.json index ddc1c22da..4553e32cd 100644 --- a/packages/plugin-auto-tracking-browser/package.json +++ b/packages/plugin-auto-tracking-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-auto-tracking-browser", - "version": "0.1.0", + "version": "0.1.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From fd20ab78215749db2c0e08d64b6f4de7badce699 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Wed, 6 Sep 2023 14:08:57 -0400 Subject: [PATCH 150/214] docs(session replay): update readme to reflect collection --- .../plugin-session-replay-browser/README.md | 10 ++++----- packages/session-replay-browser/README.md | 22 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/plugin-session-replay-browser/README.md b/packages/plugin-session-replay-browser/README.md index cf1a789c3..c70c0a205 100644 --- a/packages/plugin-session-replay-browser/README.md +++ b/packages/plugin-session-replay-browser/README.md @@ -53,7 +53,7 @@ const sessionReplayTracking = sessionReplayPlugin({ |Name|Type|Default|Description| |-|-|-|-| -|`sampleRate`|`number`|`undefined`|Use this option to control how many sessions will be selected for recording. A selected session will be recorded, while sessions that are not selected will not be recorded.

The number should be a decimal between 0 and 1, ie `0.4`, representing the fraction of sessions you would like to have randomly selected for recording. Over a large number of sessions, `0.4` would select `40%` of those sessions.| +|`sampleRate`|`number`|`undefined`|Use this option to control how many sessions will be selected for replay collection. A selected session will be collected for replay, while sessions that are not selected will not.

The number should be a decimal between 0 and 1, ie `0.4`, representing the fraction of sessions you would like to have randomly selected for replay collection. Over a large number of sessions, `0.4` would select `40%` of those sessions.| ### 3. Install plugin to Amplitude SDK @@ -62,13 +62,13 @@ amplitude.add(sessionReplayTracking); ``` ## Privacy -By default, the session recording will mask all inputs, meaning the text in inputs will appear in a session replay as asterisks: `***`. You may require more specific masking controls based on your use case, so we offer the following controls: +By default, the session replay will mask all inputs, meaning the text in inputs will appear in a session replay as asterisks: `***`. You may require more specific masking controls based on your use case, so we offer the following controls: #### 1. Unmask inputs -In your application code, add the class `.amp-unmask` to any __input__ whose text you'd like to have unmasked in the recording. In the replay of a recorded session, it will be possible to read the exact text entered into an input with this class, the text will not be converted to asterisks. +In your application code, add the class `.amp-unmask` to any __input__ whose text you'd like to have unmasked in the replay. In the session replay, it will be possible to read the exact text entered into an input with this class, the text will not be converted to asterisks. #### 2. Mask non-input elements -In your application code, add the class `.amp-mask` to any __non-input element__ whose text you'd like to have masked from the recording. The text in the element, as well as it's children, will all be converted to asterisks. +In your application code, add the class `.amp-mask` to any __non-input element__ whose text you'd like to have masked from the replay. The text in the element, as well as it's children, will all be converted to asterisks. #### 3. Block non-text elements -In your application code, add the class `.amp-block` to any element you would like to have blocked from the recording. The element will appear in the replay as a placeholder with the same dimensions. +In your application code, add the class `.amp-block` to any element you would like to have blocked from the collection of the replay. The element will appear in the replay as a placeholder with the same dimensions. diff --git a/packages/session-replay-browser/README.md b/packages/session-replay-browser/README.md index 3e4547e8a..d31d99ad9 100644 --- a/packages/session-replay-browser/README.md +++ b/packages/session-replay-browser/README.md @@ -35,9 +35,9 @@ This plugin requires that default tracking for sessions is enabled. If default t import * as sessionReplay from '@amplitude/session-replay-browser'; ``` -### 2. Initialize session recordings +### 2. Initialize session replay collection -The SDK must be configured via the following code. This call kicks off recording for the user. +The SDK must be configured via the following code. This call kicks off collection of replays for the user. ```typescript sessionReplay.init(API_KEY, { @@ -47,8 +47,8 @@ sessionReplay.init(API_KEY, { }); ``` -### 3. Get session recording event properties -Any event that occurs within the span of a session recording must be tagged with properties that signal to Amplitude to include it in the scope of the recording. The following shows an example of how to use the properties +### 3. Get session replay event properties +Any event that occurs within the span of a session replay must be tagged with properties that signal to Amplitude to include it in the scope of the replay. The following shows an example of how to use the properties ```typescript const sessionRecordingProperties = sessionReplay.getSessionRecordingProperties(); track(EVENT_NAME, { @@ -64,7 +64,7 @@ sessionReplay.setSessionId(UNIX_TIMESTAMP) ``` ### 5. Shutdown (optional) -If at any point you would like to discontinue recording, for example in a part of your application where you would not like sessions to be recorded, you can use the following method to stop recording and remove recording event listeners. +If at any point you would like to discontinue collection of session replays, for example in a part of your application where you would not like sessions to be collected, you can use the following method to stop collection and remove collection event listeners. ```typescript sessionReplay.shutdown() ``` @@ -75,21 +75,21 @@ sessionReplay.shutdown() |-|-|-|-|-| |`deviceId`|`string`|Yes|`undefined`|Sets an identifier for the device running your application.| |`sessionId`|`number`|Yes|`undefined`|Sets an identifier for the users current session. The value must be in milliseconds since epoch (Unix Timestamp).| -|`sampleRate`|`number`|No|`undefined`|Use this option to control how many sessions will be selected for recording. A selected session will be recorded, while sessions that are not selected will not be recorded.

The number should be a decimal between 0 and 1, ie `0.4`, representing the fraction of sessions you would like to have randomly selected for recording. Over a large number of sessions, `0.4` would select `40%` of those sessions.| -|`optOut`|`boolean`|No|`false`|Sets permission to record sessions. Setting a value of true prevents Amplitude from recording sessions.| +|`sampleRate`|`number`|No|`undefined`|Use this option to control how many sessions will be selected for replay collection. A selected session will be collected for replay, while sessions that are not selected will not.

The number should be a decimal between 0 and 1, ie `0.4`, representing the fraction of sessions you would like to have randomly selected for replay collection. Over a large number of sessions, `0.4` would select `40%` of those sessions.| +|`optOut`|`boolean`|No|`false`|Sets permission to collect replays for sessions. Setting a value of true prevents Amplitude from collecting session replays.| |`flushMaxRetries`|`number`|No|`5`|Sets the maximum number of retries for failed upload attempts. This is only applicable to retryable errors.| |`logLevel`|`number`|No|`LogLevel.Warn`|`LogLevel.None` or `LogLevel.Error` or `LogLevel.Warn` or `LogLevel.Verbose` or `LogLevel.Debug`. Sets the log level.| |`loggerProvider`|`Logger`|No|`Logger`|Sets a custom loggerProvider class from the Logger to emit log messages to desired destination.| |`serverZone`|`string`|No|`US`|EU or US. Sets the Amplitude server zone. Set this to EU for Amplitude projects created in EU data center.| ## Privacy -By default, the session recording will mask all inputs, meaning the text in inputs will appear in a session replay as asterisks: `***`. You may require more specific masking controls based on your use case, so we offer the following controls: +By default, the session replay will mask all inputs, meaning the text in inputs will appear in a session replay as asterisks: `***`. You may require more specific masking controls based on your use case, so we offer the following controls: #### 1. Unmask inputs -In your application code, add the class `.amp-unmask` to any __input__ whose text you'd like to have unmasked in the recording. In the replay of a recorded session, it will be possible to read the exact text entered into an input with this class, the text will not be converted to asterisks. +In your application code, add the class `.amp-unmask` to any __input__ whose text you'd like to have unmasked in the replay. In the session replay, it will be possible to read the exact text entered into an input with this class, the text will not be converted to asterisks. #### 2. Mask non-input elements -In your application code, add the class `.amp-mask` to any __non-input element__ whose text you'd like to have masked from the recording. The text in the element, as well as it's children, will all be converted to asterisks. +In your application code, add the class `.amp-mask` to any __non-input element__ whose text you'd like to have masked from the replay. The text in the element, as well as it's children, will all be converted to asterisks. #### 3. Block non-text elements -In your application code, add the class `.amp-block` to any element you would like to have blocked from the recording. The element will appear in the replay as a placeholder with the same dimensions. +In your application code, add the class `.amp-block` to any element you would like to have blocked from the collection of the replay. The element will appear in the replay as a placeholder with the same dimensions. From 8a8921efdd97a431e6095ec42e9d92e86ff2e73f Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Fri, 25 Aug 2023 12:46:44 -0400 Subject: [PATCH 151/214] refactor(session replay plugin): use session replay sdk to power plugin --- .../package.json | 1 + .../src/constants.ts | 23 - .../src/helpers.ts | 27 - .../src/session-replay.ts | 477 +---- .../src/typings/session-replay.ts | 84 - .../test/helpers.test.ts | 50 - .../test/session-replay.test.ts | 1624 +---------------- 7 files changed, 81 insertions(+), 2205 deletions(-) delete mode 100644 packages/plugin-session-replay-browser/src/helpers.ts delete mode 100644 packages/plugin-session-replay-browser/test/helpers.test.ts diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index daa5579d6..35b270a3c 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -41,6 +41,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", + "@amplitude/session-replay-browser": "0.1.0", "idb-keyval": "^6.2.1", "rrweb": "^2.0.0-alpha.11", "tslib": "^2.4.1" diff --git a/packages/plugin-session-replay-browser/src/constants.ts b/packages/plugin-session-replay-browser/src/constants.ts index 8e02f45a8..bc8692ed2 100644 --- a/packages/plugin-session-replay-browser/src/constants.ts +++ b/packages/plugin-session-replay-browser/src/constants.ts @@ -1,24 +1 @@ -import { AMPLITUDE_PREFIX } from '@amplitude/analytics-core'; -import { IDBStoreSession } from './typings/session-replay'; - -export const DEFAULT_EVENT_PROPERTY_PREFIX = '[Amplitude]'; - -export const DEFAULT_SESSION_REPLAY_PROPERTY = `${DEFAULT_EVENT_PROPERTY_PREFIX} Session Recorded`; export const DEFAULT_SESSION_START_EVENT = 'session_start'; -export const DEFAULT_SESSION_END_EVENT = 'session_end'; - -export const BLOCK_CLASS = 'amp-block'; -export const MASK_TEXT_CLASS = 'amp-mask'; -export const UNMASK_TEXT_CLASS = 'amp-unmask'; -export const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/track'; -export const SESSION_REPLAY_EU_URL = 'https://api.eu.amplitude.com/sessions/track'; -export const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; -const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 500; // derived by JSON stringifying an example payload without events -export const MAX_EVENT_LIST_SIZE_IN_BYTES = 10 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; -export const MIN_INTERVAL = 500; // 500 ms -export const MAX_INTERVAL = 10 * 1000; // 10 seconds -export const defaultSessionStore: IDBStoreSession = { - currentSequenceId: 0, - sessionSequences: {}, -}; -export const MAX_IDB_STORAGE_LENGTH = 1000 * 60 * 60 * 24 * 3; // 3 days diff --git a/packages/plugin-session-replay-browser/src/helpers.ts b/packages/plugin-session-replay-browser/src/helpers.ts deleted file mode 100644 index 3070fb1ca..000000000 --- a/packages/plugin-session-replay-browser/src/helpers.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { UNMASK_TEXT_CLASS } from './constants'; - -export const maskInputFn = (text: string, element: HTMLElement) => { - if (element.classList?.contains(UNMASK_TEXT_CLASS)) { - return text; - } - return '*'.repeat(text.length); -}; - -export const generateHashCode = function (str: string) { - let hash = 0; - if (str.length === 0) return hash; - for (let i = 0; i < str.length; i++) { - const chr = str.charCodeAt(i); - hash = (hash << 5) - hash + chr; - hash |= 0; - } - return hash; -}; - -export const isSessionInSample = function (sessionId: number, sampleRate: number) { - const hashNumber = generateHashCode(sessionId.toString()); - const absHash = Math.abs(hashNumber); - const absHashMultiply = absHash * 31; - const mod = absHashMultiply % 100; - return mod / 100 < sampleRate; -}; diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index f6103dda8..151a50c38 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -1,52 +1,14 @@ -import { getGlobalScope } from '@amplitude/analytics-client-common'; -import { BaseTransport } from '@amplitude/analytics-core'; -import { BrowserConfig, Event, ServerZone, Status } from '@amplitude/analytics-types'; -import * as IDBKeyVal from 'idb-keyval'; -import { pack, record } from 'rrweb'; -import { - BLOCK_CLASS, - DEFAULT_SESSION_END_EVENT, - DEFAULT_SESSION_REPLAY_PROPERTY, - DEFAULT_SESSION_START_EVENT, - MASK_TEXT_CLASS, - MAX_EVENT_LIST_SIZE_IN_BYTES, - MAX_IDB_STORAGE_LENGTH, - MAX_INTERVAL, - MIN_INTERVAL, - SESSION_REPLAY_EU_URL as SESSION_REPLAY_EU_SERVER_URL, - SESSION_REPLAY_SERVER_URL, - STORAGE_PREFIX, - defaultSessionStore, -} from './constants'; -import { isSessionInSample, maskInputFn } from './helpers'; -import { MAX_RETRIES_EXCEEDED_MESSAGE, STORAGE_FAILURE, UNEXPECTED_ERROR_MESSAGE, getSuccessMessage } from './messages'; -import { - Events, - IDBStore, - IDBStoreSession, - RecordingStatus, - SessionReplayContext, - SessionReplayEnrichmentPlugin, - SessionReplayOptions, - SessionReplayPlugin, -} from './typings/session-replay'; -class SessionReplay implements SessionReplayEnrichmentPlugin { +import { BrowserConfig, EnrichmentPlugin, Event } from '@amplitude/analytics-types'; +import * as sessionReplay from '@amplitude/session-replay-browser'; +import { DEFAULT_SESSION_START_EVENT } from './constants'; +import { SessionReplayOptions } from './typings/session-replay'; +export class SessionReplayPlugin implements EnrichmentPlugin { name = '@amplitude/plugin-session-replay-browser'; type = 'enrichment' as const; // this.config is defined in setup() which will always be called first // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore config: BrowserConfig; - storageKey = ''; - retryTimeout = 1000; - events: Events = []; - currentSequenceId = 0; - private scheduled: ReturnType | null = null; - queue: SessionReplayContext[] = []; - stopRecordingEvents: ReturnType | null = null; - maxPersistedEventsSize = MAX_EVENT_LIST_SIZE_IN_BYTES; - interval = MIN_INTERVAL; - timeAtLastSend: number | null = null; options: SessionReplayOptions; constructor(options?: SessionReplayOptions) { @@ -57,8 +19,6 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { config.loggerProvider.log('Installing @amplitude/plugin-session-replay.'); this.config = config; - this.config.sessionId = config.sessionId; - this.storageKey = `${STORAGE_PREFIX}_${this.config.apiKey.substring(0, 10)}`; if (typeof config.defaultTracking === 'boolean') { if (config.defaultTracking === false) { @@ -76,419 +36,40 @@ class SessionReplay implements SessionReplayEnrichmentPlugin { }; } - const globalScope = getGlobalScope(); - if (globalScope) { - globalScope.addEventListener('blur', this.blurListener); - globalScope.addEventListener('focus', this.focusListener); - } - - if (globalScope && globalScope.document && globalScope.document.hasFocus()) { - await this.initialize(true); - } - } - - blurListener = () => { - this.stopRecordingAndSendEvents(); - }; - focusListener = () => { - void this.initialize(); - }; - - stopRecordingAndSendEvents(sessionId?: number) { - try { - this.stopRecordingEvents && this.stopRecordingEvents(); - this.stopRecordingEvents = null; - } catch (error) { - const typedError = error as Error; - this.config.loggerProvider.error(`Error occurred while stopping recording: ${typedError.toString()}`); - } - const sessionIdToSend = sessionId || this.config.sessionId; - if (this.events.length && sessionIdToSend) { - this.sendEventsList({ - events: this.events, - sequenceId: this.currentSequenceId, - sessionId: sessionIdToSend, - }); - } + await sessionReplay.init(config.apiKey, { + instanceName: this.config.instanceName, + deviceId: this.config.deviceId, + optOut: this.config.optOut, + sessionId: this.config.sessionId, + loggerProvider: this.config.loggerProvider, + logLevel: this.config.logLevel, + flushMaxRetries: this.config.flushMaxRetries, + serverZone: this.config.serverZone, + sampleRate: this.options.sampleRate, + }).promise; } async execute(event: Event) { - const globalScope = getGlobalScope(); - if (globalScope && globalScope.document && !globalScope.document.hasFocus()) { - return Promise.resolve(event); - } - if (event.event_type === DEFAULT_SESSION_START_EVENT && !this.stopRecordingEvents) { - this.recordEvents(); - } else if (event.event_type === DEFAULT_SESSION_END_EVENT) { - this.stopRecordingAndSendEvents(event.session_id); - this.events = []; - this.currentSequenceId = 0; + if (event.event_type === DEFAULT_SESSION_START_EVENT && event.session_id) { + sessionReplay.setSessionId(event.session_id); } - const shouldRecord = this.getShouldRecord(); - - if (shouldRecord) { - event.event_properties = { - ...event.event_properties, - [DEFAULT_SESSION_REPLAY_PROPERTY]: true, - }; - } - - return Promise.resolve(event); - } - - async initialize(shouldSendStoredEvents = false) { - this.timeAtLastSend = Date.now(); // Initialize this so we have a point of comparison when events are recorded - if (!this.config.sessionId) { - return; - } - const storedReplaySessions = await this.getAllSessionEventsFromStore(); - // This resolves a timing issue when focus is fired multiple times in short succession, - // we only want the rest of this function to run once. We can be sure that initialize has - // already been called if this.stopRecordingEvents is defined - if (this.stopRecordingEvents) { - return; - } - const storedSequencesForSession = storedReplaySessions && storedReplaySessions[this.config.sessionId]; - if (storedReplaySessions && storedSequencesForSession && storedSequencesForSession.sessionSequences) { - const storedSeqId = storedSequencesForSession.currentSequenceId; - const lastSequence = storedSequencesForSession.sessionSequences[storedSeqId]; - if (lastSequence && lastSequence.status !== RecordingStatus.RECORDING) { - this.currentSequenceId = storedSeqId + 1; - this.events = []; - } else { - // Pick up recording where it was left off in another tab or window - this.currentSequenceId = storedSeqId; - this.events = lastSequence?.events || []; - } - } - if (shouldSendStoredEvents && storedReplaySessions) { - this.sendStoredEvents(storedReplaySessions); - } - this.recordEvents(); - } - - getShouldRecord() { - if (this.config.optOut) { - return false; - } else if (!this.config.sessionId) { - return false; - } else if (this.options && this.options.sampleRate) { - return isSessionInSample(this.config.sessionId, this.options.sampleRate); - } - return true; - } - - sendStoredEvents(storedReplaySessions: IDBStore) { - for (const sessionId in storedReplaySessions) { - const storedSequences = storedReplaySessions[sessionId].sessionSequences; - for (const storedSeqId in storedSequences) { - const seq = storedSequences[storedSeqId]; - const numericSeqId = parseInt(storedSeqId, 10); - const numericSessionId = parseInt(sessionId, 10); - if (numericSessionId === this.config.sessionId && numericSeqId === this.currentSequenceId) { - continue; - } - if (seq.events.length && seq.status === RecordingStatus.RECORDING) { - this.sendEventsList({ - events: seq.events, - sequenceId: numericSeqId, - sessionId: numericSessionId, - }); - } - } - } - } - - recordEvents() { - const shouldRecord = this.getShouldRecord(); - if (!shouldRecord && this.config.sessionId) { - this.config.loggerProvider.log(`Opting session ${this.config.sessionId} out of recording.`); - return; - } - this.stopRecordingEvents = record({ - emit: (event) => { - const globalScope = getGlobalScope(); - if (globalScope && globalScope.document && !globalScope.document.hasFocus()) { - this.stopRecordingAndSendEvents(); - return; - } - const eventString = JSON.stringify(event); - - const shouldSplit = this.shouldSplitEventsList(eventString); - if (shouldSplit) { - this.sendEventsList({ - events: this.events, - sequenceId: this.currentSequenceId, - sessionId: this.config.sessionId as number, - }); - this.events = []; - this.currentSequenceId++; - } - this.events.push(eventString); - void this.storeEventsForSession(this.events, this.currentSequenceId, this.config.sessionId as number); - }, - packFn: pack, - maskAllInputs: true, - maskTextClass: MASK_TEXT_CLASS, - blockClass: BLOCK_CLASS, - maskInputFn, - recordCanvas: false, - errorHandler: (error) => { - const typedError = error as Error; - this.config.loggerProvider.error('Error while recording: ', typedError.toString()); - - return true; - }, - }); - } - - /** - * Determines whether to send the events list to the backend and start a new - * empty events list, based on the size of the list as well as the last time sent - * @param nextEventString - * @returns boolean - */ - shouldSplitEventsList = (nextEventString: string): boolean => { - const sizeOfNextEvent = new Blob([nextEventString]).size; - const sizeOfEventsList = new Blob(this.events).size; - if (sizeOfEventsList + sizeOfNextEvent >= this.maxPersistedEventsSize) { - return true; - } - if (this.timeAtLastSend !== null && Date.now() - this.timeAtLastSend > this.interval && this.events.length) { - this.interval = Math.min(MAX_INTERVAL, this.interval + MIN_INTERVAL); - this.timeAtLastSend = Date.now(); - return true; - } - return false; - }; - - sendEventsList({ events, sequenceId, sessionId }: { events: string[]; sequenceId: number; sessionId: number }) { - this.addToQueue({ - events, - sequenceId, - attempts: 0, - timeout: 0, - sessionId, - }); - } - - addToQueue(...list: SessionReplayContext[]) { - const tryable = list.filter((context) => { - if (context.attempts < this.config.flushMaxRetries) { - context.attempts += 1; - return true; - } - this.completeRequest({ - context, - err: `${MAX_RETRIES_EXCEEDED_MESSAGE}, batch sequence id, ${context.sequenceId}`, - }); - return false; - }); - tryable.forEach((context) => { - this.queue = this.queue.concat(context); - if (context.timeout === 0) { - this.schedule(0); - return; - } - - setTimeout(() => { - context.timeout = 0; - this.schedule(0); - }, context.timeout); - }); - } - - schedule(timeout: number) { - if (this.scheduled) return; - this.scheduled = setTimeout(() => { - void this.flush(true).then(() => { - if (this.queue.length > 0) { - this.schedule(timeout); - } - }); - }, timeout); - } - - async flush(useRetry = false) { - const list: SessionReplayContext[] = []; - const later: SessionReplayContext[] = []; - this.queue.forEach((context) => (context.timeout === 0 ? list.push(context) : later.push(context))); - this.queue = later; - - if (this.scheduled) { - clearTimeout(this.scheduled); - this.scheduled = null; - } - - await Promise.all(list.map((context) => this.send(context, useRetry))); - } - - getServerUrl() { - if (this.config.serverZone === ServerZone.EU) { - return SESSION_REPLAY_EU_SERVER_URL; - } - return SESSION_REPLAY_SERVER_URL; - } - - async send(context: SessionReplayContext, useRetry = true) { - const payload = { - api_key: this.config.apiKey, - device_id: this.config.deviceId, - session_id: context.sessionId, - start_timestamp: context.sessionId, - events_batch: { - version: 1, - events: context.events, - seq_number: context.sequenceId, - }, + const sessionRecordingProperties = sessionReplay.getSessionRecordingProperties(); + event.event_properties = { + ...event.event_properties, + ...sessionRecordingProperties, }; - try { - const options: RequestInit = { - headers: { - 'Content-Type': 'application/json', - Accept: '*/*', - }, - body: JSON.stringify(payload), - method: 'POST', - }; - const server_url = this.getServerUrl(); - const res = await fetch(server_url, options); - if (res === null) { - this.completeRequest({ context, err: UNEXPECTED_ERROR_MESSAGE }); - return; - } - if (!useRetry) { - let responseBody = ''; - try { - responseBody = JSON.stringify(res.body, null, 2); - } catch { - // to avoid crash, but don't care about the error, add comment to avoid empty block lint error - } - this.completeRequest({ context, success: `${res.status}: ${responseBody}` }); - } else { - this.handleReponse(res.status, context); - } - } catch (e) { - this.completeRequest({ context, err: e as string }); - } - } - - handleReponse(status: number, context: SessionReplayContext) { - const parsedStatus = new BaseTransport().buildStatus(status); - switch (parsedStatus) { - case Status.Success: - this.handleSuccessResponse(context); - break; - default: - this.handleOtherResponse(context); - } - } - - handleSuccessResponse(context: SessionReplayContext) { - this.completeRequest({ context, success: getSuccessMessage(context.sessionId) }); - } - - handleOtherResponse(context: SessionReplayContext) { - this.addToQueue({ - ...context, - timeout: context.attempts * this.retryTimeout, - }); - } - async getAllSessionEventsFromStore() { - try { - const storedReplaySessionContexts: IDBStore | undefined = await IDBKeyVal.get(this.storageKey); - - return storedReplaySessionContexts; - } catch (e) { - this.config.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); - } - return undefined; - } - - async storeEventsForSession(events: Events, sequenceId: number, sessionId: number) { - try { - await IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore = {}): IDBStore => { - const session: IDBStoreSession = sessionMap[sessionId] || { ...defaultSessionStore }; - session.currentSequenceId = sequenceId; - - const currentSequence = (session.sessionSequences && session.sessionSequences[sequenceId]) || {}; - - currentSequence.events = events; - currentSequence.status = RecordingStatus.RECORDING; - - return { - ...sessionMap, - [sessionId]: { - ...session, - sessionSequences: { - ...session.sessionSequences, - [sequenceId]: currentSequence, - }, - }, - }; - }); - } catch (e) { - this.config.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); - } - } - - async cleanUpSessionEventsStore(sessionId: number, sequenceId: number) { - try { - await IDBKeyVal.update(this.storageKey, (sessionMap: IDBStore = {}): IDBStore => { - const session: IDBStoreSession = sessionMap[sessionId]; - const sequenceToUpdate = session?.sessionSequences && session.sessionSequences[sequenceId]; - if (!sequenceToUpdate) { - return sessionMap; - } - - sequenceToUpdate.events = []; - sequenceToUpdate.status = RecordingStatus.SENT; - - // Delete sent sequences for current session - Object.entries(session.sessionSequences).forEach(([storedSeqId, sequence]) => { - const numericStoredSeqId = parseInt(storedSeqId, 10); - if (sequence.status === RecordingStatus.SENT && sequenceId !== numericStoredSeqId) { - delete session.sessionSequences[numericStoredSeqId]; - } - }); - - // Delete any sessions that are older than 3 days - Object.keys(sessionMap).forEach((sessionId: string) => { - const numericSessionId = parseInt(sessionId, 10); - if (Date.now() - numericSessionId >= MAX_IDB_STORAGE_LENGTH) { - delete sessionMap[numericSessionId]; - } - }); - - return sessionMap; - }); - } catch (e) { - this.config.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); - } - } - - completeRequest({ context, err, success }: { context: SessionReplayContext; err?: string; success?: string }) { - context.sessionId && this.cleanUpSessionEventsStore(context.sessionId, context.sequenceId); - if (err) { - this.config.loggerProvider.error(err); - } else if (success) { - this.config.loggerProvider.log(success); - } + return Promise.resolve(event); } - async teardown() { - const globalScope = getGlobalScope(); - if (globalScope) { - globalScope.removeEventListener('blur', this.blurListener); - globalScope.removeEventListener('focus', this.focusListener); - } - - this.stopRecordingAndSendEvents(); + async teardown(): Promise { + sessionReplay.shutdown(); } } -export const sessionReplayPlugin: SessionReplayPlugin = (options?: SessionReplayOptions) => { - return new SessionReplay(options); +export const sessionReplayPlugin: (options?: SessionReplayOptions) => EnrichmentPlugin = ( + options?: SessionReplayOptions, +) => { + return new SessionReplayPlugin(options); }; diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index c9fcc63ab..e4cfd5f5a 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -1,87 +1,3 @@ -import { BrowserConfig, EnrichmentPlugin } from '@amplitude/analytics-types'; -import { record } from 'rrweb'; - export interface SessionReplayOptions { sampleRate?: number; } - -export type Events = string[]; - -export interface SessionReplayContext { - events: Events; - sequenceId: number; - attempts: number; - timeout: number; - sessionId: number; -} - -export enum RecordingStatus { - RECORDING = 'recording', - SENT = 'sent', -} - -export interface IDBStoreSequence { - events: Events; - status: RecordingStatus; -} - -export interface IDBStoreSession { - currentSequenceId: number; - sessionSequences: { - [sequenceId: number]: IDBStoreSequence; - }; -} - -export interface IDBStore { - [sessionId: number]: IDBStoreSession; -} -export interface SessionReplayEnrichmentPlugin extends EnrichmentPlugin { - setup: (config: BrowserConfig) => Promise; - config: BrowserConfig; - storageKey: string; - retryTimeout: number; - events: Events; - currentSequenceId: number; - interval: number; - queue: SessionReplayContext[]; - timeAtLastSend: number | null; - stopRecordingEvents: ReturnType | null; - stopRecordingAndSendEvents: (sessionId?: number) => void; - maxPersistedEventsSize: number; - initialize: (shouldSendStoredEvents?: boolean) => Promise; - sendStoredEvents: (storedReplaySessions: IDBStore) => void; - getShouldRecord: () => boolean; - recordEvents: () => void; - shouldSplitEventsList: (nextEventString: string) => boolean; - sendEventsList: ({ - events, - sequenceId, - sessionId, - }: { - events: string[]; - sequenceId: number; - sessionId: number; - }) => void; - addToQueue: (...list: SessionReplayContext[]) => void; - schedule: (timeout: number) => void; - flush: (useRetry?: boolean) => Promise; - send: (context: SessionReplayContext, useRetry?: boolean) => Promise; - completeRequest({ - context, - err, - success, - removeEvents, - }: { - context: SessionReplayContext; - err?: string | undefined; - success?: string | undefined; - removeEvents?: boolean | undefined; - }): void; - getAllSessionEventsFromStore: () => Promise; - storeEventsForSession: (events: Events, sequenceId: number, sessionId: number) => Promise; - cleanUpSessionEventsStore: (sessionId: number, sequenceId: number) => Promise; -} - -export interface SessionReplayPlugin { - (options?: SessionReplayOptions): SessionReplayEnrichmentPlugin; -} diff --git a/packages/plugin-session-replay-browser/test/helpers.test.ts b/packages/plugin-session-replay-browser/test/helpers.test.ts deleted file mode 100644 index a51e63faa..000000000 --- a/packages/plugin-session-replay-browser/test/helpers.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { UNMASK_TEXT_CLASS } from '../src/constants'; -import { generateHashCode, isSessionInSample, maskInputFn } from '../src/helpers'; - -describe('SessionReplayPlugin helpers', () => { - describe('maskInputFn', () => { - test('should not mask an element whose class list has amp-unmask in it', () => { - const htmlElement = document.createElement('div'); - htmlElement.classList.add(UNMASK_TEXT_CLASS); - const result = maskInputFn('some text', htmlElement); - expect(result).toEqual('some text'); - }); - test('should mask any other element', () => { - const htmlElement = document.createElement('div'); - htmlElement.classList.add('another-class'); - const result = maskInputFn('some text', htmlElement); - expect(result).toEqual('*********'); - }); - test('should handle an element without a class list', () => { - const htmlElement = {} as unknown as HTMLElement; - const result = maskInputFn('some text', htmlElement); - expect(result).toEqual('*********'); - }); - }); - - describe('generateHashCode', () => { - test('should return 0 if string length is 0', () => { - const hashCode = generateHashCode(''); - expect(hashCode).toEqual(0); - }); - test('should return hash for numeric string', () => { - const hashCode = generateHashCode('1691093770366'); - expect(hashCode).toEqual(139812688); - }); - test('should return hash for alphabetic string', () => { - const hashCode = generateHashCode('my_session_identifier'); - expect(hashCode).toEqual(989939557); - }); - }); - - describe('isSessionInSample', () => { - test('should deterministically return true if calculation puts session id below sample rate', () => { - const result = isSessionInSample(1691092433788, 0.5); - expect(result).toEqual(true); - }); - test('should deterministically return false if calculation puts session id above sample rate', () => { - const result = isSessionInSample(1691092416403, 0.5); - expect(result).toEqual(false); - }); - }); -}); diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index a65ca0042..ef072c19b 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -1,62 +1,36 @@ -/* eslint-disable jest/expect-expect */ -import * as AnalyticsClientCommon from '@amplitude/analytics-client-common'; +/* eslint-disable @typescript-eslint/no-unused-vars */ import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; -import { BrowserConfig, LogLevel, Logger, ServerZone } from '@amplitude/analytics-types'; -import * as IDBKeyVal from 'idb-keyval'; -import * as RRWeb from 'rrweb'; -import { DEFAULT_SESSION_REPLAY_PROPERTY } from '../src/constants'; -import * as Helpers from '../src/helpers'; -import { UNEXPECTED_ERROR_MESSAGE, getSuccessMessage } from '../src/messages'; -import { sessionReplayPlugin } from '../src/session-replay'; -import { IDBStore, RecordingStatus } from '../src/typings/session-replay'; +import { BrowserConfig, LogLevel, Logger } from '@amplitude/analytics-types'; +import * as sessionReplayBrowser from '@amplitude/session-replay-browser'; +import { SessionReplayPlugin, sessionReplayPlugin } from '../src/session-replay'; -jest.mock('idb-keyval'); -type MockedIDBKeyVal = jest.Mocked; - -jest.mock('rrweb'); -type MockedRRWeb = jest.Mocked; +jest.mock('@amplitude/session-replay-browser'); +type MockedSessionReplayBrowser = jest.Mocked; type MockedLogger = jest.Mocked; -const mockEvent = { - type: 4, - data: { href: 'https://analytics.amplitude.com/', width: 1728, height: 154 }, - timestamp: 1687358660935, -}; -const mockEventString = JSON.stringify(mockEvent); - -async function runScheduleTimers() { - // exhause first setTimeout - jest.runAllTimers(); - // wait for next tick to call nested setTimeout - await Promise.resolve(); - // exhause nested setTimeout - jest.runAllTimers(); -} - describe('SessionReplayPlugin', () => { - const { get, update } = IDBKeyVal as MockedIDBKeyVal; - const { record } = RRWeb as MockedRRWeb; - let originalFetch: typeof global.fetch; - let mockLoggerProvider: MockedLogger; - let addEventListenerMock: jest.Mock; - let removeEventListenerMock: jest.Mock; + const { init, setSessionId, getSessionRecordingProperties, shutdown } = + sessionReplayBrowser as MockedSessionReplayBrowser; + const mockLoggerProvider: MockedLogger = { + error: jest.fn(), + log: jest.fn(), + disable: jest.fn(), + enable: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }; const mockConfig: BrowserConfig = { apiKey: 'static_key', flushIntervalMillis: 0, flushMaxRetries: 1, flushQueueSize: 0, logLevel: LogLevel.None, - loggerProvider: { - error: jest.fn(), - log: jest.fn(), - disable: jest.fn(), - enable: jest.fn(), - warn: jest.fn(), - debug: jest.fn(), - }, + loggerProvider: mockLoggerProvider, optOut: false, + deviceId: '1a2b3c', serverUrl: 'url', + serverZone: 'US', transportProvider: new FetchTransport(), useBatch: false, sessionId: 123, @@ -75,342 +49,29 @@ describe('SessionReplayPlugin', () => { }, }; beforeEach(() => { + init.mockReturnValue({ + promise: Promise.resolve(), + }); jest.useFakeTimers(); - originalFetch = global.fetch; - global.fetch = jest.fn(() => - Promise.resolve({ - status: 200, - }), - ) as jest.Mock; - addEventListenerMock = jest.fn() as typeof addEventListenerMock; - removeEventListenerMock = jest.fn() as typeof removeEventListenerMock; - jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue({ - addEventListener: addEventListenerMock, - removeEventListener: removeEventListenerMock, - document: { - hasFocus: () => true, - }, - } as unknown as typeof globalThis); - mockLoggerProvider = { - error: jest.fn(), - log: jest.fn(), - disable: jest.fn(), - enable: jest.fn(), - warn: jest.fn(), - debug: jest.fn(), - }; }); afterEach(() => { jest.resetAllMocks(); - jest.spyOn(global.Math, 'random').mockRestore(); - global.fetch = originalFetch; jest.useRealTimers(); }); describe('setup', () => { test('should setup plugin', async () => { - const sessionReplay = sessionReplayPlugin(); + const sessionReplay = new SessionReplayPlugin(); await sessionReplay.setup(mockConfig); expect(sessionReplay.config.transportProvider).toBeDefined(); expect(sessionReplay.config.serverUrl).toBe('url'); expect(sessionReplay.config.flushMaxRetries).toBe(1); expect(sessionReplay.config.flushQueueSize).toBe(0); expect(sessionReplay.config.flushIntervalMillis).toBe(0); - expect(sessionReplay.storageKey).toBe('AMP_replay_unsent_static_key'); - }); - - test('should call initalize with shouldSendStoredEvents=true', async () => { - const sessionReplay = sessionReplayPlugin(); - const initalize = jest.spyOn(sessionReplay, 'initialize').mockReturnValueOnce(Promise.resolve()); - await sessionReplay.setup(mockConfig); - - expect(initalize).toHaveBeenCalledTimes(1); - - expect(initalize.mock.calls[0]).toEqual([true]); - }); - test('should set up blur and focus event listeners', async () => { - const sessionReplay = sessionReplayPlugin(); - const stopRecordingMock = jest.fn(); - sessionReplay.stopRecordingEvents = stopRecordingMock; - const initialize = jest.spyOn(sessionReplay, 'initialize').mockReturnValueOnce(Promise.resolve()); - await sessionReplay.setup(mockConfig); - initialize.mockReset(); - expect(addEventListenerMock).toHaveBeenCalledTimes(2); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(addEventListenerMock.mock.calls[0][0]).toEqual('blur'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment - const blurCallback = addEventListenerMock.mock.calls[0][1]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - blurCallback(); - expect(stopRecordingMock).toHaveBeenCalled(); - expect(sessionReplay.stopRecordingEvents).toEqual(null); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(addEventListenerMock.mock.calls[1][0]).toEqual('focus'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment - const focusCallback = addEventListenerMock.mock.calls[1][1]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - focusCallback(); - expect(initialize).toHaveBeenCalled(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(initialize.mock.calls[0]).toEqual([]); - }); - test('it should not call initialize if the document does not have focus', () => { - const sessionReplay = sessionReplayPlugin(); - const initialize = jest.spyOn(sessionReplay, 'initialize').mockReturnValueOnce(Promise.resolve()); - jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue({ - document: { - hasFocus: () => false, - }, - } as typeof globalThis); - expect(initialize).not.toHaveBeenCalled(); - }); - }); - - describe('initalize', () => { - test('should read events from storage and send them if shouldSendStoredEvents is true', async () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - sessionId: 456, - }; - sessionReplay.config = config; - const mockGetResolution: Promise = Promise.resolve({ - 123: { - currentSequenceId: 3, - sessionSequences: { - 3: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - 456: { - currentSequenceId: 1, - sessionSequences: { - 1: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - }); - get.mockReturnValueOnce(mockGetResolution); - const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); - - await sessionReplay.initialize(true); - await mockGetResolution; - jest.runAllTimers(); - expect(send).toHaveBeenCalledTimes(1); - - // Should send only events from sequences marked as recording and not current session - expect(send.mock.calls[0][0]).toEqual({ - attempts: 1, - events: [mockEventString], - sequenceId: 3, - sessionId: 123, - timeout: 0, - }); }); - test('should return early if using old format of IDBStore', async () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - sessionId: 456, - }; - sessionReplay.config = config; - const mockGetResolution = Promise.resolve({ - 123: { - events: [mockEventString], - sequenceId: 1, - }, - 456: { - events: [mockEventString], - sequenceId: 1, - }, - }); - get.mockReturnValueOnce(mockGetResolution); - const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); - await sessionReplay.initialize(true); - await mockGetResolution; - jest.runAllTimers(); - expect(send).toHaveBeenCalledTimes(0); - }); - test('should return early if session id not set', async () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - sessionId: undefined, - }; - sessionReplay.config = config; - const getAllSessionEventsFromStore = jest - .spyOn(sessionReplay, 'getAllSessionEventsFromStore') - .mockReturnValueOnce(Promise.resolve({})); - await sessionReplay.initialize(); - expect(getAllSessionEventsFromStore).not.toHaveBeenCalled(); - }); - test('should return early if stopRecordingEvents is already defined, signaling that recording is already in progress', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - // eslint-disable-next-line @typescript-eslint/no-empty-function - sessionReplay.stopRecordingEvents = () => {}; - const getAllSessionEventsFromStore = jest - .spyOn(sessionReplay, 'getAllSessionEventsFromStore') - .mockReturnValueOnce( - Promise.resolve({ - 123: { - shouldRecord: true, - currentSequenceId: 3, - sessionSequences: { - 3: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - }), - ); - await sessionReplay.initialize(); - expect(getAllSessionEventsFromStore).toHaveBeenCalled(); - expect(sessionReplay.events).toEqual([]); // events should not be updated to match what is in the store - }); - test('should configure current sequence id and events correctly if last sequence was sent', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const mockGetResolution: Promise = Promise.resolve({ - 123: { - currentSequenceId: 3, - sessionSequences: { - 3: { - events: [mockEventString], - status: RecordingStatus.SENT, - }, - }, - }, - }); - get.mockReturnValueOnce(mockGetResolution); - await sessionReplay.initialize(); - expect(sessionReplay.currentSequenceId).toEqual(4); - expect(sessionReplay.events).toEqual([]); - }); - test('should configure current sequence id and events correctly if last sequence was recording', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const mockGetResolution: Promise = Promise.resolve({ - 123: { - currentSequenceId: 3, - sessionSequences: { - 3: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - }); - get.mockReturnValueOnce(mockGetResolution); - await sessionReplay.initialize(); - expect(sessionReplay.currentSequenceId).toEqual(3); - expect(sessionReplay.events).toEqual([mockEventString]); - }); - test('should handle no stored events', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const mockGetResolution = Promise.resolve({}); - get.mockReturnValueOnce(mockGetResolution); - await sessionReplay.initialize(); - expect(sessionReplay.currentSequenceId).toBe(0); - expect(sessionReplay.events).toEqual([]); - }); - test('should handle no stored sequences for session', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const mockGetResolution = Promise.resolve({ - 123: { - currentSequenceId: 0, - sessionSequences: {}, - }, - }); - get.mockReturnValueOnce(mockGetResolution); - await sessionReplay.initialize(); - expect(sessionReplay.currentSequenceId).toBe(0); - expect(sessionReplay.events).toEqual([]); - }); - test('should record events', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const mockGetResolution = Promise.resolve({}); - get.mockReturnValueOnce(mockGetResolution); - await sessionReplay.initialize(); - expect(record).toHaveBeenCalledTimes(1); - }); - describe('sendStoredEvents', () => { - test('should send all recording sequences except the current sequence for the current session', () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - sessionId: 456, - }; - sessionReplay.config = config; - sessionReplay.currentSequenceId = 3; - const store: IDBStore = { - 123: { - currentSequenceId: 5, - sessionSequences: { - 3: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - 4: { - events: [], - status: RecordingStatus.SENT, - }, - 5: { - events: [mockEventString, mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - 456: { - currentSequenceId: 3, - sessionSequences: { - 1: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - 2: { - events: [], - status: RecordingStatus.SENT, - }, - 3: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - }; - const sendEventsList = jest.spyOn(sessionReplay, 'sendEventsList'); - sessionReplay.sendStoredEvents(store); - expect(sendEventsList).toHaveBeenCalledTimes(3); - expect(sendEventsList.mock.calls[0][0]).toEqual({ - events: [mockEventString], - sequenceId: 3, - sessionId: 123, - }); - expect(sendEventsList.mock.calls[1][0]).toEqual({ - events: [mockEventString, mockEventString], - sequenceId: 5, - sessionId: 123, - }); - expect(sendEventsList.mock.calls[2][0]).toEqual({ - events: [mockEventString], - sequenceId: 1, - sessionId: 456, - }); - }); - }); describe('defaultTracking', () => { test('should not change defaultTracking if its set to true', async () => { - const sessionReplay = sessionReplayPlugin(); + const sessionReplay = new SessionReplayPlugin(); await sessionReplay.setup({ ...mockConfig, defaultTracking: true, @@ -418,7 +79,7 @@ describe('SessionReplayPlugin', () => { expect(sessionReplay.config.defaultTracking).toBe(true); }); test('should modify defaultTracking to enable sessions if its set to false', async () => { - const sessionReplay = sessionReplayPlugin(); + const sessionReplay = new SessionReplayPlugin(); await sessionReplay.setup({ ...mockConfig, defaultTracking: false, @@ -431,7 +92,7 @@ describe('SessionReplayPlugin', () => { }); }); test('should modify defaultTracking to enable sessions if it is an object', async () => { - const sessionReplay = sessionReplayPlugin(); + const sessionReplay = new SessionReplayPlugin(); await sessionReplay.setup({ ...mockConfig, defaultTracking: { @@ -444,107 +105,36 @@ describe('SessionReplayPlugin', () => { }); }); }); - }); - describe('getShouldRecord', () => { - test('should return true if no options', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const shouldRecord = sessionReplay.getShouldRecord(); - expect(shouldRecord).toBe(true); - }); - test('should return false if session not included in sample rate', () => { - jest.spyOn(Helpers, 'isSessionInSample').mockImplementationOnce(() => false); - const sessionReplay = sessionReplayPlugin({ - sampleRate: 0.2, - }); - sessionReplay.config = mockConfig; - const shouldRecord = sessionReplay.getShouldRecord(); - expect(shouldRecord).toBe(false); - }); - test('should set record as true if session is included in sample rate', () => { - jest.spyOn(Helpers, 'isSessionInSample').mockImplementationOnce(() => true); - const sessionReplay = sessionReplayPlugin({ - sampleRate: 0.8, - }); - sessionReplay.config = mockConfig; - const shouldRecord = sessionReplay.getShouldRecord(); - expect(shouldRecord).toBe(true); - }); - test('should set record as false if opt out in config', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = { - ...mockConfig, - optOut: true, - }; - const shouldRecord = sessionReplay.getShouldRecord(); - expect(shouldRecord).toBe(false); - }); - test('should set record as false if no session id', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = { - ...mockConfig, - sessionId: undefined, - }; - const shouldRecord = sessionReplay.getShouldRecord(); - expect(shouldRecord).toBe(false); - }); - test('opt out in config should override the sample rate', () => { - jest.spyOn(Math, 'random').mockImplementationOnce(() => 0.7); - const sessionReplay = sessionReplayPlugin({ - sampleRate: 0.8, + test('should call initalize on session replay sdk', async () => { + const sessionReplay = new SessionReplayPlugin({ + sampleRate: 0.4, }); - sessionReplay.config = { - ...mockConfig, - optOut: true, - }; - const shouldRecord = sessionReplay.getShouldRecord(); - expect(shouldRecord).toBe(false); - }); - }); + await sessionReplay.setup(mockConfig); - describe('stopRecordingAndSendEvents', () => { - test('it should catch errors', () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - loggerProvider: mockLoggerProvider, - }; - sessionReplay.config = config; - const mockStopRecordingEvents = jest.fn().mockImplementation(() => { - throw new Error('test error'); + expect(init).toHaveBeenCalledTimes(1); + + expect(init.mock.calls[0][0]).toEqual(mockConfig.apiKey); + expect(init.mock.calls[0][1]).toEqual({ + deviceId: mockConfig.deviceId, + flushMaxRetries: mockConfig.flushMaxRetries, + logLevel: mockConfig.logLevel, + loggerProvider: mockConfig.loggerProvider, + optOut: mockConfig.optOut, + sampleRate: 0.4, + serverZone: mockConfig.serverZone, + sessionId: mockConfig.sessionId, }); - sessionReplay.stopRecordingEvents = mockStopRecordingEvents; - sessionReplay.stopRecordingAndSendEvents(); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalled(); }); }); describe('execute', () => { - test('it should return event if document does not have focus', async () => { - const sessionReplay = sessionReplayPlugin(); - jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue({ - addEventListener: addEventListenerMock, - document: { - hasFocus: () => false, - }, - } as unknown as typeof globalThis); - await sessionReplay.setup(mockConfig); - const event = { - event_type: 'event_type', - event_properties: { - property_a: true, - property_b: 123, - }, - }; - - const executedEvent = await sessionReplay.execute(event); - expect(executedEvent).toEqual(event); - }); test('should add event property for [Amplitude] Session Recorded', async () => { const sessionReplay = sessionReplayPlugin(); await sessionReplay.setup(mockConfig); + getSessionRecordingProperties.mockReturnValueOnce({ + '[Amplitude] Session Recorded': true, + }); const event = { event_type: 'event_type', event_properties: { @@ -561,1137 +151,25 @@ describe('SessionReplayPlugin', () => { }); }); - test('should restart recording events when session_start fires', async () => { - const sessionReplay = sessionReplayPlugin(); - const mockGetResolution = Promise.resolve({}); - get.mockReturnValueOnce(mockGetResolution); + test('should set the session id on session replay sdk when session_start fires', async () => { + const sessionReplay = new SessionReplayPlugin(); await sessionReplay.setup(mockConfig); - await mockGetResolution; - jest.runAllTimers(); const event = { event_type: 'session_start', - }; - await sessionReplay.execute(event); - expect(record).toHaveBeenCalledTimes(2); - }); - - test('should send the current events list when session_end fires and stop recording events', async () => { - const sessionReplay = sessionReplayPlugin(); - const mockStopRecordingEvents = jest.fn(); - sessionReplay.stopRecordingEvents = mockStopRecordingEvents; - sessionReplay.config = mockConfig; - const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); - - const event = { - event_type: 'session_end', - session_id: 789, - }; - sessionReplay.events = [mockEventString]; - await sessionReplay.execute(event); - expect(mockStopRecordingEvents).toHaveBeenCalledTimes(1); - jest.runAllTimers(); - expect(send).toHaveBeenCalledTimes(1); - - // Sending first stored session events - expect(send.mock.calls[0][0]).toEqual({ - attempts: 1, - events: [mockEventString], - sequenceId: 0, - sessionId: 789, - timeout: 0, - }); - }); - }); - - describe('recordEvents', () => { - test('should store events in class and in IDB', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - sessionReplay.recordEvents(); - expect(sessionReplay.events).toEqual([]); - const recordArg = record.mock.calls[0][0]; - // Emit event, which is stored in class and IDB - recordArg?.emit && recordArg?.emit(mockEvent); - expect(sessionReplay.events).toEqual([mockEventString]); - expect(update).toHaveBeenCalledTimes(1); - expect(update.mock.calls[0][1]({})).toEqual({ - 123: { - currentSequenceId: 0, - sessionSequences: { - 0: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - }); - }); - - test('should split the events list at an increasing interval and send', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - sessionReplay.timeAtLastSend = new Date('2023-07-31 08:30:00').getTime(); - sessionReplay.recordEvents(); - jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:30:00').getTime()); - const sendEventsList = jest.spyOn(sessionReplay, 'sendEventsList'); - const recordArg = record.mock.calls[0][0]; - // Emit first event, which is not sent immediately - recordArg?.emit && recordArg?.emit(mockEvent); - expect(sendEventsList).toHaveBeenCalledTimes(0); - // Emit second event and advance timers to interval - jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:31:00').getTime()); - recordArg?.emit && recordArg?.emit(mockEvent); - expect(sendEventsList).toHaveBeenCalledTimes(1); - expect(sendEventsList).toHaveBeenCalledWith({ - events: [mockEventString], - sequenceId: 0, - sessionId: 123, - }); - expect(sessionReplay.events).toEqual([mockEventString]); - expect(sessionReplay.currentSequenceId).toEqual(1); - }); - - test('should split the events list at max size and send', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - sessionReplay.maxPersistedEventsSize = 20; - // Simulate as if many events have already been built up - const events = ['#'.repeat(20)]; - sessionReplay.events = events; - // eslint-disable-next-line @typescript-eslint/no-empty-function - const sendEventsListMock = jest.spyOn(sessionReplay, 'sendEventsList').mockImplementationOnce(() => {}); - sessionReplay.recordEvents(); - - const recordArg = record.mock.calls[0][0]; - recordArg?.emit && recordArg?.emit(mockEvent); - - expect(sendEventsListMock).toHaveBeenCalledTimes(1); - expect(sendEventsListMock).toHaveBeenCalledWith({ - events, - sequenceId: 0, - sessionId: 123, - }); - - expect(sessionReplay.events).toEqual([mockEventString]); - expect(update).toHaveBeenCalledTimes(1); - expect(update.mock.calls[0][1]({})).toEqual({ - 123: { - currentSequenceId: 1, - sessionSequences: { - 1: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - }); - }); - - test('should stop recording and send events if document is not in focus', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - sessionReplay.recordEvents(); - const stopRecordingMock = jest.fn(); - sessionReplay.stopRecordingEvents = stopRecordingMock; - expect(sessionReplay.events).toEqual([]); - sessionReplay.events = [mockEventString]; // Add one event to list to trigger sending in stopRecordingAndSendEvents - jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue({ - document: { - hasFocus: () => false, - }, - } as typeof globalThis); - // eslint-disable-next-line @typescript-eslint/no-empty-function - const sendEventsListMock = jest.spyOn(sessionReplay, 'sendEventsList').mockImplementationOnce(() => {}); - const recordArg = record.mock.calls[0][0]; - recordArg?.emit && recordArg?.emit(mockEvent); - expect(sendEventsListMock).toHaveBeenCalledTimes(1); - expect(sendEventsListMock).toHaveBeenCalledWith({ - events: [mockEventString], - sequenceId: 0, - sessionId: 123, - }); - expect(stopRecordingMock).toHaveBeenCalled(); - expect(sessionReplay.stopRecordingEvents).toEqual(null); - expect(sessionReplay.events).toEqual([mockEventString]); // events should not change, emmitted event should be ignored - }); - - test('should add an error handler', () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - loggerProvider: mockLoggerProvider, - }; - sessionReplay.config = config; - sessionReplay.recordEvents(); - const recordArg = record.mock.calls[0][0]; - const errorHandlerReturn = recordArg?.errorHandler && recordArg?.errorHandler(new Error('test error')); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalled(); - expect(errorHandlerReturn).toBe(true); - }); - }); - - describe('addToQueue', () => { - test('should add to queue and schedule a flush', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const schedule = jest.spyOn(sessionReplay, 'schedule').mockReturnValueOnce(undefined); - const context = { - events: [mockEventString], - sequenceId: 1, - sessionId: 123, - attempts: 0, - timeout: 0, - }; - sessionReplay.addToQueue(context); - expect(schedule).toHaveBeenCalledTimes(1); - expect(schedule).toHaveBeenCalledWith(0); - expect(context.attempts).toBe(1); - }); - - test('should not add to queue if attemps are greater than allowed retries', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = { - ...mockConfig, - flushMaxRetries: 1, - }; - const completeRequest = jest.spyOn(sessionReplay, 'completeRequest').mockReturnValueOnce(undefined); - const context = { - events: [mockEventString], - sequenceId: 1, - sessionId: 123, - attempts: 1, - timeout: 0, - }; - sessionReplay.addToQueue(context); - expect(completeRequest).toHaveBeenCalledTimes(1); - expect(completeRequest).toHaveBeenCalledWith({ - context: { - events: [mockEventString], - sequenceId: 1, - sessionId: 123, - attempts: 1, - timeout: 0, - }, - err: 'Session replay event batch rejected due to exceeded retry count, batch sequence id, 1', - }); - }); - }); - - describe('schedule', () => { - test('should schedule a flush', async () => { - const sessionReplay = sessionReplayPlugin(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (sessionReplay as any).scheduled = null; - sessionReplay.queue = [ - { - events: [mockEventString], - sequenceId: 1, - sessionId: 123, - attempts: 0, - timeout: 0, - }, - ]; - const flush = jest - .spyOn(sessionReplay, 'flush') - .mockImplementationOnce(() => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (sessionReplay as any).scheduled = null; - return Promise.resolve(undefined); - }) - .mockReturnValueOnce(Promise.resolve(undefined)); - sessionReplay.schedule(0); - await runScheduleTimers(); - expect(flush).toHaveBeenCalledTimes(2); - }); - - test('should not schedule if one is already in progress', () => { - const sessionReplay = sessionReplayPlugin(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (sessionReplay as any).scheduled = setTimeout(jest.fn, 0); - const flush = jest.spyOn(sessionReplay, 'flush').mockReturnValueOnce(Promise.resolve(undefined)); - sessionReplay.schedule(0); - expect(flush).toHaveBeenCalledTimes(0); - }); - }); - - describe('flush', () => { - test('should call send', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - sessionReplay.queue = [ - { - events: [mockEventString], - sequenceId: 1, - sessionId: 123, - attempts: 0, - timeout: 0, - }, - ]; - const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); - const result = await sessionReplay.flush(); - expect(sessionReplay.queue).toEqual([]); - expect(result).toBe(undefined); - expect(send).toHaveBeenCalledTimes(1); - }); - - test('should send later', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - sessionReplay.queue = [ - { - events: [mockEventString], - sequenceId: 1, - sessionId: 123, - attempts: 0, - timeout: 1000, - }, - ]; - const send = jest.spyOn(sessionReplay, 'send').mockReturnValueOnce(Promise.resolve()); - const result = await sessionReplay.flush(); - expect(sessionReplay.queue).toEqual([ - { - events: [mockEventString], - sequenceId: 1, - sessionId: 123, - attempts: 0, - timeout: 1000, - }, - ]); - expect(result).toBe(undefined); - expect(send).toHaveBeenCalledTimes(0); - }); - }); - - describe('send', () => { - test('should make a request correctly', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const context = { - events: [mockEventString], - sequenceId: 1, - sessionId: 123, - attempts: 0, - timeout: 0, - }; - - await sessionReplay.send(context); - expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch).toHaveBeenCalledWith('https://api-secure.amplitude.com/sessions/track', { - body: JSON.stringify({ - api_key: 'static_key', - session_id: 123, - start_timestamp: 123, - events_batch: { version: 1, events: [mockEventString], seq_number: 1 }, - }), - headers: { Accept: '*/*', 'Content-Type': 'application/json' }, - method: 'POST', - }); - }); - test('should make a request to eu', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = { - ...mockConfig, - serverZone: ServerZone.EU, - }; - - const context = { - events: [mockEventString], - sequenceId: 1, - sessionId: 123, - attempts: 0, - timeout: 0, - }; - - await sessionReplay.send(context); - expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch).toHaveBeenCalledWith('https://api.eu.amplitude.com/sessions/track', { - body: JSON.stringify({ - api_key: 'static_key', - session_id: 123, - start_timestamp: 123, - events_batch: { version: 1, events: [mockEventString], seq_number: 1 }, - }), - headers: { Accept: '*/*', 'Content-Type': 'application/json' }, - method: 'POST', - }); - }); - test('should update IDB store upon success', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const context = { - events: [mockEventString], - sequenceId: 1, - sessionId: 123, - attempts: 0, - timeout: 0, - }; - const cleanUpSessionEventsStore = jest - .spyOn(sessionReplay, 'cleanUpSessionEventsStore') - .mockReturnValueOnce(Promise.resolve()); - await sessionReplay.send(context); - jest.runAllTimers(); - expect(cleanUpSessionEventsStore).toHaveBeenCalledTimes(1); - expect(cleanUpSessionEventsStore.mock.calls[0]).toEqual([123, 1]); - }); - test('should remove session events from IDB store upon failure', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const context = { - events: [mockEventString], - sequenceId: 1, - sessionId: 123, - attempts: 0, - timeout: 0, - }; - const cleanUpSessionEventsStore = jest - .spyOn(sessionReplay, 'cleanUpSessionEventsStore') - .mockReturnValueOnce(Promise.resolve()); - (global.fetch as jest.Mock).mockImplementationOnce(() => Promise.reject()); - - await sessionReplay.send(context); - jest.runAllTimers(); - expect(cleanUpSessionEventsStore).toHaveBeenCalledTimes(1); - expect(cleanUpSessionEventsStore.mock.calls[0]).toEqual([123, 1]); - }); - - test('should retry if retry param is true', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const context = { - events: [mockEventString], - sequenceId: 1, - sessionId: 123, - attempts: 0, - timeout: 0, - }; - (global.fetch as jest.Mock) - .mockImplementationOnce(() => - Promise.resolve({ - status: 500, - }), - ) - .mockImplementationOnce(() => - Promise.resolve({ - status: 200, - }), - ); - const addToQueue = jest.spyOn(sessionReplay, 'addToQueue'); - - await sessionReplay.send(context, true); - expect(addToQueue).toHaveBeenCalledTimes(1); - expect(addToQueue).toHaveBeenCalledWith({ - ...context, - attempts: 1, - timeout: 0, - }); - await runScheduleTimers(); - }); - - test('should not retry if retry param is false', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const context = { - events: [mockEventString], - sequenceId: 1, - sessionId: 123, - attempts: 0, - timeout: 0, - }; - (global.fetch as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ - status: 500, - }), - ); - const addToQueue = jest.spyOn(sessionReplay, 'addToQueue'); - - await sessionReplay.send(context, false); - expect(addToQueue).toHaveBeenCalledTimes(0); - }); - }); - - describe('module level integration', () => { - describe('with a sample rate', () => { - test('should not record session if excluded due to sampling', async () => { - jest.spyOn(Helpers, 'isSessionInSample').mockImplementation(() => false); - const sessionReplay = sessionReplayPlugin({ - sampleRate: 0.2, - }); - await sessionReplay.setup(mockConfig); - const event = await sessionReplay.execute({ - event_type: 'my_event', - session_id: 123, - }); - // Confirm Amplitude event is not tagged with Session Recorded - expect(event).not.toMatchObject({ - event_properties: { - [DEFAULT_SESSION_REPLAY_PROPERTY]: true, - }, - }); - expect(record).not.toHaveBeenCalled(); - expect(update).not.toHaveBeenCalled(); - const endEvent = await sessionReplay.execute({ - event_type: 'session_end', - session_id: 123, - }); - // Confirm Amplitude event is not tagged with Session Recorded - expect(endEvent).not.toMatchObject({ - event_properties: { - [DEFAULT_SESSION_REPLAY_PROPERTY]: true, - }, - }); - await runScheduleTimers(); - expect(fetch).not.toHaveBeenCalled(); - }); - test('should record session if included due to sampling', async () => { - jest.spyOn(Helpers, 'isSessionInSample').mockImplementation(() => true); - (fetch as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve({ - status: 200, - }); - }); - const sessionReplay = sessionReplayPlugin({ - sampleRate: 0.8, - }); - const config = { - ...mockConfig, - loggerProvider: mockLoggerProvider, - }; - await sessionReplay.setup(config); - const startEvent = await sessionReplay.execute({ - event_type: 'session_start', - session_id: 456, - }); - // Confirm Amplitude event is tagged with Session Recorded - expect(startEvent?.event_properties).toMatchObject({ - [DEFAULT_SESSION_REPLAY_PROPERTY]: true, - }); - // Log is called from setup, but that's not what we're testing here - mockLoggerProvider.log.mockClear(); - expect(record).toHaveBeenCalled(); - const recordArg = record.mock.calls[0][0]; - recordArg?.emit && recordArg?.emit(mockEvent); - const endEvent = await sessionReplay.execute({ - event_type: 'session_end', - session_id: 456, - }); - // Confirm Amplitude event is tagged with Session Recorded - expect(endEvent?.event_properties).toMatchObject({ - [DEFAULT_SESSION_REPLAY_PROPERTY]: true, - }); - await runScheduleTimers(); - expect(fetch).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.log).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.log.mock.calls[0][0]).toEqual(getSuccessMessage(456)); - }); - }); - - describe('with optOut in config', () => { - test('should not record session if excluded due to optOut', async () => { - const sessionReplay = sessionReplayPlugin(); - await sessionReplay.setup({ - ...mockConfig, - optOut: true, - }); - await sessionReplay.execute({ - event_type: 'session_start', - session_id: 456, - }); - expect(record).not.toHaveBeenCalled(); - await sessionReplay.execute({ - event_type: 'session_end', - session_id: 456, - }); - await runScheduleTimers(); - expect(fetch).not.toHaveBeenCalled(); - }); - }); - test('should handle unexpected error', async () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - loggerProvider: mockLoggerProvider, - }; - (fetch as jest.Mock).mockImplementationOnce(() => Promise.reject('API Failure')); - await sessionReplay.setup(config); - sessionReplay.events = [mockEventString]; - await sessionReplay.execute({ - event_type: 'session_end', - session_id: 456, - }); - await runScheduleTimers(); - expect(fetch).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual('API Failure'); - }); - test('should handle retry for 400 error', async () => { - (fetch as jest.Mock) - .mockImplementationOnce(() => { - return Promise.resolve({ - status: 400, - }); - }) - .mockImplementationOnce(() => { - return Promise.resolve({ - status: 200, - }); - }); - const sessionReplay = sessionReplayPlugin(); - sessionReplay.retryTimeout = 10; - const config = { - ...mockConfig, - flushMaxRetries: 2, - loggerProvider: mockLoggerProvider, - }; - await sessionReplay.setup(config); - // Log is called from setup, but that's not what we're testing here - mockLoggerProvider.log.mockClear(); - sessionReplay.events = [mockEventString]; - await sessionReplay.execute({ - event_type: 'session_end', - session_id: 456, - }); - await runScheduleTimers(); - expect(fetch).toHaveBeenCalledTimes(2); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(0); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.log).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.log.mock.calls[0][0]).toEqual(getSuccessMessage(456)); - }); - test('should handle retry for 413 error with flushQueueSize of 1', async () => { - (fetch as jest.Mock) - .mockImplementationOnce(() => { - return Promise.resolve({ - status: 413, - }); - }) - .mockImplementationOnce(() => { - return Promise.resolve({ - status: 200, - }); - }); - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - flushMaxRetries: 2, - }; - await sessionReplay.setup(config); - sessionReplay.events = [mockEventString]; - const event = { - event_type: 'session_end', - session_id: 456, - }; - await sessionReplay.execute(event); - await runScheduleTimers(); - expect(fetch).toHaveBeenCalledTimes(2); - }); - test('should handle retry for 500 error', async () => { - (fetch as jest.Mock) - .mockImplementationOnce(() => { - return Promise.resolve({ - status: 500, - }); - }) - .mockImplementationOnce(() => { - return Promise.resolve({ - status: 200, - }); - }); - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - flushMaxRetries: 2, - }; - await sessionReplay.setup(config); - sessionReplay.events = [mockEventString]; - const event = { - event_type: 'session_end', - session_id: 456, - }; - await sessionReplay.execute(event); - await runScheduleTimers(); - expect(fetch).toHaveBeenCalledTimes(2); - }); - test('should handle retry for 503 error', async () => { - (fetch as jest.Mock) - .mockImplementationOnce(() => { - return Promise.resolve({ - status: 503, - }); - }) - .mockImplementationOnce(() => { - return Promise.resolve({ - status: 200, - }); - }); - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - flushMaxRetries: 2, - }; - await sessionReplay.setup(config); - sessionReplay.events = [mockEventString]; - const event = { - event_type: 'session_end', - session_id: 456, - }; - await sessionReplay.execute(event); - await runScheduleTimers(); - expect(fetch).toHaveBeenCalledTimes(2); - }); - test('should handle unexpected error where fetch response is null', async () => { - (fetch as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve(null); - }); - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - flushMaxRetries: 2, - loggerProvider: mockLoggerProvider, - }; - await sessionReplay.setup(config); - sessionReplay.events = [mockEventString]; - const event = { - event_type: 'session_end', session_id: 456, }; await sessionReplay.execute(event); - await runScheduleTimers(); - expect(fetch).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual(UNEXPECTED_ERROR_MESSAGE); - }); - }); - - describe('getAllSessionEventsFromStore', () => { - test('should catch errors', async () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - loggerProvider: mockLoggerProvider, - }; - sessionReplay.config = config; - get.mockImplementationOnce(() => Promise.reject('error')); - await sessionReplay.getAllSessionEventsFromStore(); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( - 'Failed to store session replay events in IndexedDB: error', - ); - }); - }); - - describe('cleanUpSessionEventsStore', () => { - test('should update events and status for current session', async () => { - jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:30:00').getTime()); - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const currentSessionId = new Date('2023-07-31 07:30:00').getTime(); - const mockIDBStore: IDBStore = { - [currentSessionId]: { - currentSequenceId: 3, - sessionSequences: { - 2: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - 3: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - }; - await sessionReplay.cleanUpSessionEventsStore(currentSessionId, 3); - - expect(update).toHaveBeenCalledTimes(1); - expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ - [currentSessionId]: { - currentSequenceId: 3, - sessionSequences: { - 2: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - 3: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, - }); - }); - - test('should delete sent sequences for current session', async () => { - jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:30:00').getTime()); - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const currentSessionId = new Date('2023-07-31 07:30:00').getTime(); - const mockIDBStore: IDBStore = { - [currentSessionId]: { - currentSequenceId: 3, - sessionSequences: { - 2: { - events: [], - status: RecordingStatus.SENT, - }, - 3: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - }; - await sessionReplay.cleanUpSessionEventsStore(currentSessionId, 3); - - expect(update).toHaveBeenCalledTimes(1); - expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ - [currentSessionId]: { - currentSequenceId: 3, - sessionSequences: { - 3: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, - }); - }); - - test('should keep sessions that are less than 3 days old, and delete sessions that are more than 3 days old', async () => { - jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:30:00').getTime()); - const fourDayOldSessionId = new Date('2023-07-27 07:30:00').getTime(); - const oneDayOldSessionId = new Date('2023-07-30 07:30:00').getTime(); - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const mockIDBStore: IDBStore = { - [oneDayOldSessionId]: { - currentSequenceId: 3, - sessionSequences: { - 3: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - [fourDayOldSessionId]: { - currentSequenceId: 3, - sessionSequences: { - 3: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, - }; - await sessionReplay.cleanUpSessionEventsStore(oneDayOldSessionId, 3); - - expect(update).toHaveBeenCalledTimes(1); - expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ - [oneDayOldSessionId]: { - currentSequenceId: 3, - sessionSequences: { - 3: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, - }); - }); - test('should catch errors', async () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - loggerProvider: mockLoggerProvider, - }; - sessionReplay.config = config; - update.mockImplementationOnce(() => Promise.reject('error')); - await sessionReplay.cleanUpSessionEventsStore(123, 1); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( - 'Failed to store session replay events in IndexedDB: error', - ); - }); - test('should handle an undefined store', async () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - loggerProvider: mockLoggerProvider, - }; - sessionReplay.config = config; - update.mockImplementationOnce(() => Promise.resolve()); - await sessionReplay.cleanUpSessionEventsStore(123, 1); - expect(update.mock.calls[0][1](undefined)).toEqual({}); - }); - }); - - describe('storeEventsForSession', () => { - test('should update the session current sequence id, and the current sequence events and status', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const mockIDBStore: IDBStore = { - 123: { - currentSequenceId: 2, - sessionSequences: { - 2: { - events: [], - status: RecordingStatus.RECORDING, - }, - }, - }, - 456: { - currentSequenceId: 1, - sessionSequences: { - 1: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, - }; - await sessionReplay.storeEventsForSession([mockEventString], 2, 123); - - expect(update).toHaveBeenCalledTimes(1); - expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ - 123: { - currentSequenceId: 2, - sessionSequences: { - 2: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - 456: { - currentSequenceId: 1, - sessionSequences: { - 1: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, - }); - }); - test('should add a new entry if none exist for sequence id', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const mockIDBStore: IDBStore = { - 123: { - currentSequenceId: 1, - sessionSequences: { - 1: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, - 456: { - currentSequenceId: 1, - sessionSequences: { - 1: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, - }; - await sessionReplay.storeEventsForSession([mockEventString], 2, 123); - - expect(update).toHaveBeenCalledTimes(1); - expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ - 123: { - currentSequenceId: 2, - sessionSequences: { - 1: { - events: [], - status: RecordingStatus.SENT, - }, - 2: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - 456: { - currentSequenceId: 1, - sessionSequences: { - 1: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, - }); - }); - test('should add a new entry if none exist for session id', async () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.config = mockConfig; - const mockIDBStore: IDBStore = { - 123: { - currentSequenceId: 2, - sessionSequences: { - 2: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, - }; - await sessionReplay.storeEventsForSession([mockEventString], 0, 456); - - expect(update).toHaveBeenCalledTimes(1); - expect(update.mock.calls[0][1](mockIDBStore)).toEqual({ - 123: { - currentSequenceId: 2, - sessionSequences: { - 2: { - events: [], - status: RecordingStatus.SENT, - }, - }, - }, - 456: { - currentSequenceId: 0, - sessionSequences: { - 0: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - }); - }); - test('should catch errors', async () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - loggerProvider: mockLoggerProvider, - }; - sessionReplay.config = config; - update.mockImplementationOnce(() => Promise.reject('error')); - await sessionReplay.storeEventsForSession([mockEventString], 0, config.sessionId as number); - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( - 'Failed to store session replay events in IndexedDB: error', - ); - }); - test('should handle an undefined store', async () => { - const sessionReplay = sessionReplayPlugin(); - const config = { - ...mockConfig, - loggerProvider: mockLoggerProvider, - }; - sessionReplay.config = config; - update.mockImplementationOnce(() => Promise.resolve()); - await sessionReplay.storeEventsForSession([mockEventString], 0, 456); - expect(update.mock.calls[0][1](undefined)).toEqual({ - 456: { - currentSequenceId: 0, - sessionSequences: { - 0: { - events: [mockEventString], - status: RecordingStatus.RECORDING, - }, - }, - }, - }); - }); - }); - - describe('shouldSplitEventsList', () => { - describe('event list size', () => { - test('should return true if size of events list plus size of next event is over the max size', () => { - const sessionReplay = sessionReplayPlugin(); - const eventsList = ['#'.repeat(20)]; - sessionReplay.events = eventsList; - sessionReplay.maxPersistedEventsSize = 20; - const nextEvent = 'a'; - const result = sessionReplay.shouldSplitEventsList(nextEvent); - expect(result).toBe(true); - }); - test('should return false if size of events list plus size of next event is under the max size', () => { - const sessionReplay = sessionReplayPlugin(); - const eventsList = ['#'.repeat(20)]; - sessionReplay.events = eventsList; - sessionReplay.maxPersistedEventsSize = 22; - const nextEvent = 'a'; - const result = sessionReplay.shouldSplitEventsList(nextEvent); - expect(result).toBe(false); - }); - }); - describe('interval', () => { - test('should return false if timeAtLastSend is null', () => { - const sessionReplay = sessionReplayPlugin(); - const nextEvent = 'a'; - const result = sessionReplay.shouldSplitEventsList(nextEvent); - expect(result).toBe(false); - }); - test('should return false if it has not been long enough since last send', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.timeAtLastSend = new Date('2023-07-31 08:30:00').getTime(); - jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:30:00').getTime()); - const nextEvent = 'a'; - const result = sessionReplay.shouldSplitEventsList(nextEvent); - expect(result).toBe(false); - }); - test('should return true if it has been long enough since last send and events have been emitted', () => { - const sessionReplay = sessionReplayPlugin(); - sessionReplay.events = [mockEventString]; - sessionReplay.timeAtLastSend = new Date('2023-07-31 08:30:00').getTime(); - jest.useFakeTimers().setSystemTime(new Date('2023-07-31 08:33:00').getTime()); - const nextEvent = 'a'; - const result = sessionReplay.shouldSplitEventsList(nextEvent); - expect(result).toBe(true); - }); + expect(setSessionId).toHaveBeenCalledTimes(1); + expect(setSessionId).toHaveBeenCalledWith(456); }); }); describe('teardown', () => { - test('should remove event listeners', async () => { + test('should call session replay teardown', async () => { const sessionReplay = sessionReplayPlugin(); await sessionReplay.setup(mockConfig); await sessionReplay.teardown?.(); - expect(removeEventListenerMock).toHaveBeenCalledTimes(2); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(removeEventListenerMock.mock.calls[0][0]).toEqual('blur'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(removeEventListenerMock.mock.calls[1][0]).toEqual('focus'); - }); - - test('should stop recording and send any events in queue', async () => { - const sessionReplay = sessionReplayPlugin(); - await sessionReplay.setup(mockConfig); - const stopRecordingMock = jest.fn(); - sessionReplay.stopRecordingEvents = stopRecordingMock; - sessionReplay.events = [mockEventString]; - // eslint-disable-next-line @typescript-eslint/no-empty-function - const sendEventsListMock = jest.spyOn(sessionReplay, 'sendEventsList').mockImplementationOnce(() => {}); - await sessionReplay.teardown?.(); - expect(stopRecordingMock).toHaveBeenCalled(); - expect(sessionReplay.stopRecordingEvents).toBe(null); - expect(sendEventsListMock).toHaveBeenCalled(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(sendEventsListMock.mock.calls[0][0]).toEqual({ - events: [mockEventString], - sequenceId: 0, - sessionId: 123, - }); + expect(shutdown).toHaveBeenCalled(); }); }); }); From 2c638300880234d3aaf784f72fc04e4deaaec221 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 31 Aug 2023 13:24:35 -0400 Subject: [PATCH 152/214] fix(session replay plugin): update dependencies and remove eslint override --- packages/plugin-session-replay-browser/package.json | 3 +-- .../plugin-session-replay-browser/test/session-replay.test.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 35b270a3c..c7737e960 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -38,8 +38,6 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": ">=1 <3", - "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", "@amplitude/session-replay-browser": "0.1.0", "idb-keyval": "^6.2.1", @@ -47,6 +45,7 @@ "tslib": "^2.4.1" }, "devDependencies": { + "@amplitude/analytics-client-common": ">=1 <3", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index ef072c19b..35e1cd17d 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; import { BrowserConfig, LogLevel, Logger } from '@amplitude/analytics-types'; import * as sessionReplayBrowser from '@amplitude/session-replay-browser'; From 41faedceaba39ea743a83d734fc0462b2fb072df Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 31 Aug 2023 13:28:18 -0400 Subject: [PATCH 153/214] fix(session replay plugin): delete unused file --- packages/plugin-session-replay-browser/src/messages.ts | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 packages/plugin-session-replay-browser/src/messages.ts diff --git a/packages/plugin-session-replay-browser/src/messages.ts b/packages/plugin-session-replay-browser/src/messages.ts deleted file mode 100644 index dd1de2e45..000000000 --- a/packages/plugin-session-replay-browser/src/messages.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const getSuccessMessage = (sessionId: number) => - `Session replay event batch tracked successfully for session id ${sessionId}`; -export const UNEXPECTED_ERROR_MESSAGE = 'Unexpected error occurred'; -export const MAX_RETRIES_EXCEEDED_MESSAGE = 'Session replay event batch rejected due to exceeded retry count'; -export const STORAGE_FAILURE = 'Failed to store session replay events in IndexedDB'; From f26ab9cf2135f550eac91938a44d37972308339d Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Tue, 5 Sep 2023 11:06:11 -0400 Subject: [PATCH 154/214] fix(session replay plugin): remove use of client-common --- packages/plugin-session-replay-browser/package.json | 1 - .../test/session-replay.test.ts | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index c7737e960..13777d703 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -45,7 +45,6 @@ "tslib": "^2.4.1" }, "devDependencies": { - "@amplitude/analytics-client-common": ">=1 <3", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 35e1cd17d..91148644b 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -1,4 +1,3 @@ -import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; import { BrowserConfig, LogLevel, Logger } from '@amplitude/analytics-types'; import * as sessionReplayBrowser from '@amplitude/session-replay-browser'; import { SessionReplayPlugin, sessionReplayPlugin } from '../src/session-replay'; @@ -30,13 +29,11 @@ describe('SessionReplayPlugin', () => { deviceId: '1a2b3c', serverUrl: 'url', serverZone: 'US', - transportProvider: new FetchTransport(), useBatch: false, sessionId: 123, cookieExpiration: 365, cookieSameSite: 'Lax', cookieSecure: false, - cookieStorage: new CookieStorage(), cookieUpgrade: true, disableCookies: false, domain: '.amplitude.com', @@ -46,7 +43,7 @@ describe('SessionReplayPlugin', () => { language: true, platform: true, }, - }; + } as unknown as BrowserConfig; beforeEach(() => { init.mockReturnValue({ promise: Promise.resolve(), From ada95d70bbafbb700d593579b8165e51916fa0c7 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Wed, 6 Sep 2023 14:18:06 -0400 Subject: [PATCH 155/214] fix(session replay plugin): test --- .../plugin-session-replay-browser/test/session-replay.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 91148644b..2232b18fb 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -58,7 +58,6 @@ describe('SessionReplayPlugin', () => { test('should setup plugin', async () => { const sessionReplay = new SessionReplayPlugin(); await sessionReplay.setup(mockConfig); - expect(sessionReplay.config.transportProvider).toBeDefined(); expect(sessionReplay.config.serverUrl).toBe('url'); expect(sessionReplay.config.flushMaxRetries).toBe(1); expect(sessionReplay.config.flushQueueSize).toBe(0); From 13dbb29be5916f57b58703ecae3f4deb7f45359e Mon Sep 17 00:00:00 2001 From: Andrey Sokolov Date: Fri, 8 Sep 2023 22:26:37 +0400 Subject: [PATCH 156/214] fix: config values should take precedence over nativemodule in Context plugin (#575) --- packages/analytics-react-native/src/plugins/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/analytics-react-native/src/plugins/context.ts b/packages/analytics-react-native/src/plugins/context.ts index 2419b775e..4760c85f3 100644 --- a/packages/analytics-react-native/src/plugins/context.ts +++ b/packages/analytics-react-native/src/plugins/context.ts @@ -65,7 +65,7 @@ export class Context implements BeforePlugin { async execute(context: Event): Promise { const time = new Date().getTime(); const nativeContext = await this.nativeModule?.getApplicationContext(this.config.trackingOptions); - const appVersion = nativeContext?.version || this.config.appVersion; + const appVersion = this.config.appVersion || nativeContext?.version; const platform = nativeContext?.platform || BROWSER_PLATFORM; const osName = nativeContext?.osName || this.uaResult.browser.name; const osVersion = nativeContext?.osVersion || this.uaResult.browser.version; From f361d34b9158827f298df4486d2f5110af1a880e Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Fri, 8 Sep 2023 19:13:37 +0000 Subject: [PATCH 157/214] chore(release): publish - @amplitude/analytics-react-native@1.4.4 - @amplitude/plugin-session-replay-browser@0.6.7 - @amplitude/session-replay-browser@0.1.1 --- packages/analytics-react-native/CHANGELOG.md | 13 +++++++++++++ packages/analytics-react-native/package.json | 2 +- packages/analytics-react-native/src/version.ts | 2 +- .../plugin-session-replay-browser/CHANGELOG.md | 18 ++++++++++++++++++ .../plugin-session-replay-browser/package.json | 6 ++++-- packages/session-replay-browser/CHANGELOG.md | 9 +++++++++ packages/session-replay-browser/package.json | 2 +- 7 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/analytics-react-native/CHANGELOG.md b/packages/analytics-react-native/CHANGELOG.md index 4bc7c070b..b422a3488 100644 --- a/packages/analytics-react-native/CHANGELOG.md +++ b/packages/analytics-react-native/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.4.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-react-native@1.4.3...@amplitude/analytics-react-native@1.4.4) (2023-09-08) + +### Bug Fixes + +- config values should take precedence over nativemodule in Context plugin + ([#575](https://github.com/amplitude/Amplitude-TypeScript/issues/575)) + ([13dbb29](https://github.com/amplitude/Amplitude-TypeScript/commit/13dbb29be5916f57b58703ecae3f4deb7f45359e)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.4.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-react-native@1.4.2...@amplitude/analytics-react-native@1.4.3) (2023-08-22) **Note:** Version bump only for package @amplitude/analytics-react-native diff --git a/packages/analytics-react-native/package.json b/packages/analytics-react-native/package.json index 6cb439f33..4a7330734 100644 --- a/packages/analytics-react-native/package.json +++ b/packages/analytics-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-react-native", - "version": "1.4.3", + "version": "1.4.4", "description": "Official React Native SDK", "keywords": [ "analytics", diff --git a/packages/analytics-react-native/src/version.ts b/packages/analytics-react-native/src/version.ts index f1b477811..4d7b20880 100644 --- a/packages/analytics-react-native/src/version.ts +++ b/packages/analytics-react-native/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.4.3'; +export const VERSION = '1.4.4'; diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index da9580c11..0e125375d 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,24 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.6.7](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.6...@amplitude/plugin-session-replay-browser@0.6.7) (2023-09-08) + +### Bug Fixes + +- **session replay plugin:** delete unused file + ([41faedc](https://github.com/amplitude/Amplitude-TypeScript/commit/41faedceaba39ea743a83d734fc0462b2fb072df)) +- **session replay plugin:** remove use of client-common + ([f26ab9c](https://github.com/amplitude/Amplitude-TypeScript/commit/f26ab9cf2135f550eac91938a44d37972308339d)) +- **session replay plugin:** test + ([ada95d7](https://github.com/amplitude/Amplitude-TypeScript/commit/ada95d70bbafbb700d593579b8165e51916fa0c7)) +- **session replay plugin:** update dependencies and remove eslint override + ([2c63830](https://github.com/amplitude/Amplitude-TypeScript/commit/2c638300880234d3aaf784f72fc04e4deaaec221)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.6.6](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.5...@amplitude/plugin-session-replay-browser@0.6.6) (2023-08-22) **Note:** Version bump only for package @amplitude/plugin-session-replay-browser diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 13777d703..f6281e449 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.6.6", + "version": "0.6.7", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -38,8 +38,10 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { + "@amplitude/analytics-client-common": ">=1 <3", + "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "0.1.0", + "@amplitude/session-replay-browser": "^0.1.1", "idb-keyval": "^6.2.1", "rrweb": "^2.0.0-alpha.11", "tslib": "^2.4.1" diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index 6891a0028..b87836ed8 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.1.0...@amplitude/session-replay-browser@0.1.1) (2023-09-08) + +**Note:** Version bump only for package @amplitude/session-replay-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # 0.1.0 (2023-08-25) ### Bug Fixes diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index 7157b6c92..06fe26ec0 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "0.1.0", + "version": "0.1.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From ab6c0643e6a2b869cc10a719c2edec25b2de2e25 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Mon, 11 Sep 2023 15:43:01 -0400 Subject: [PATCH 158/214] feat(session replay): add getSessionReplayProperties method --- packages/session-replay-browser/README.md | 4 +-- .../src/session-replay.ts | 8 ++++- .../test/session-replay.test.ts | 36 +++++++++++++++++-- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/session-replay-browser/README.md b/packages/session-replay-browser/README.md index d31d99ad9..aaa79dd4d 100644 --- a/packages/session-replay-browser/README.md +++ b/packages/session-replay-browser/README.md @@ -50,10 +50,10 @@ sessionReplay.init(API_KEY, { ### 3. Get session replay event properties Any event that occurs within the span of a session replay must be tagged with properties that signal to Amplitude to include it in the scope of the replay. The following shows an example of how to use the properties ```typescript -const sessionRecordingProperties = sessionReplay.getSessionRecordingProperties(); +const sessionReplayProperties = sessionReplay.getSessionReplayProperties(); track(EVENT_NAME, { ...eventProperties, - ...sessionRecordingProperties + ...sessionReplayProperties }) ``` diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index 0b8fe7e50..4dca0f350 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -91,7 +91,7 @@ export class SessionReplay implements AmplitudeSessionReplay { this.recordEvents(); } - getSessionRecordingProperties() { + getSessionReplayProperties() { if (!this.config) { this.loggerProvider.error('Session replay init has not been called, cannot get session recording properties.'); return {}; @@ -107,6 +107,12 @@ export class SessionReplay implements AmplitudeSessionReplay { return {}; } + getSessionRecordingProperties = () => { + this.loggerProvider.warn('Please use getSessionReplayProperties instead of getSessionRecordingProperties.'); + + return this.getSessionReplayProperties(); + }; + blurListener = () => { this.stopRecordingAndSendEvents(); }; diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index 545a031fc..d0118c787 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -186,6 +186,38 @@ describe('SessionReplayPlugin', () => { }); }); + describe('getSessionReplayProperties', () => { + test('should return an empty object if config not set', () => { + const sessionReplay = new SessionReplay(); + sessionReplay.loggerProvider = mockLoggerProvider; + + const result = sessionReplay.getSessionReplayProperties(); + expect(result).toEqual({}); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockLoggerProvider.error).toHaveBeenCalled(); + }); + + test('should return an empty object if shouldRecord is false', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, mockOptions).promise; + sessionReplay.getShouldRecord = () => false; + + const result = sessionReplay.getSessionReplayProperties(); + expect(result).toEqual({}); + }); + + test('should return the session recorded property if shouldRecord is true', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, mockOptions).promise; + sessionReplay.getShouldRecord = () => true; + + const result = sessionReplay.getSessionReplayProperties(); + expect(result).toEqual({ + '[Amplitude] Session Recorded': true, + }); + }); + }); + describe('getSessionRecordingProperties', () => { test('should return an empty object if config not set', () => { const sessionReplay = new SessionReplay(); @@ -1088,7 +1120,7 @@ describe('SessionReplayPlugin', () => { jest.spyOn(Helpers, 'isSessionInSample').mockImplementation(() => false); const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, { ...mockOptions, sampleRate: 0.2 }).promise; - const sessionRecordingProperties = sessionReplay.getSessionRecordingProperties(); + const sessionRecordingProperties = sessionReplay.getSessionReplayProperties(); expect(sessionRecordingProperties).toMatchObject({}); expect(record).not.toHaveBeenCalled(); expect(update).not.toHaveBeenCalled(); @@ -1104,7 +1136,7 @@ describe('SessionReplayPlugin', () => { }); const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, { ...mockOptions, sampleRate: 0.8 }).promise; - const sessionRecordingProperties = sessionReplay.getSessionRecordingProperties(); + const sessionRecordingProperties = sessionReplay.getSessionReplayProperties(); expect(sessionRecordingProperties).toMatchObject({ [DEFAULT_SESSION_REPLAY_PROPERTY]: true, }); From 13f2829fcd08d72896544607aa155eec3e0f0ea8 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Mon, 11 Sep 2023 20:10:54 +0000 Subject: [PATCH 159/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.6.8 - @amplitude/session-replay-browser@0.2.0 --- packages/plugin-session-replay-browser/CHANGELOG.md | 9 +++++++++ packages/plugin-session-replay-browser/package.json | 4 ++-- packages/session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/session-replay-browser/package.json | 2 +- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 0e125375d..aab7fe0ff 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.6.8](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.7...@amplitude/plugin-session-replay-browser@0.6.8) (2023-09-11) + +**Note:** Version bump only for package @amplitude/plugin-session-replay-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.6.7](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.6...@amplitude/plugin-session-replay-browser@0.6.7) (2023-09-08) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index f6281e449..f35162552 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.6.7", + "version": "0.6.8", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -41,7 +41,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "^0.1.1", + "@amplitude/session-replay-browser": "^0.2.0", "idb-keyval": "^6.2.1", "rrweb": "^2.0.0-alpha.11", "tslib": "^2.4.1" diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index b87836ed8..325078bbf 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.2.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.1.1...@amplitude/session-replay-browser@0.2.0) (2023-09-11) + +### Features + +- **session replay:** add getSessionReplayProperties method + ([ab6c064](https://github.com/amplitude/Amplitude-TypeScript/commit/ab6c0643e6a2b869cc10a719c2edec25b2de2e25)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.1.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.1.0...@amplitude/session-replay-browser@0.1.1) (2023-09-08) **Note:** Version bump only for package @amplitude/session-replay-browser diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index 06fe26ec0..510855b49 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "0.1.1", + "version": "0.2.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 597201a04b5c7c51b3f047caaf39986af6fb6534 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Tue, 12 Sep 2023 11:24:25 -0400 Subject: [PATCH 160/214] fix(session replay): expose getSessionReplayProperties externally --- packages/session-replay-browser/src/index.ts | 3 ++- .../session-replay-browser/src/session-replay-factory.ts | 5 +++++ .../session-replay-browser/src/typings/session-replay.ts | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/session-replay-browser/src/index.ts b/packages/session-replay-browser/src/index.ts index e712f2a02..0cd9bdbf7 100644 --- a/packages/session-replay-browser/src/index.ts +++ b/packages/session-replay-browser/src/index.ts @@ -1,2 +1,3 @@ import sessionReplay from './session-replay-factory'; -export const { init, setSessionId, getSessionRecordingProperties, shutdown } = sessionReplay; +export const { init, setSessionId, getSessionRecordingProperties, getSessionReplayProperties, shutdown } = + sessionReplay; diff --git a/packages/session-replay-browser/src/session-replay-factory.ts b/packages/session-replay-browser/src/session-replay-factory.ts index 8e4517c55..b7b57db5a 100644 --- a/packages/session-replay-browser/src/session-replay-factory.ts +++ b/packages/session-replay-browser/src/session-replay-factory.ts @@ -27,6 +27,11 @@ const createInstance: () => AmplitudeSessionReplay = () => { 'getSessionRecordingProperties', getLogConfig(sessionReplay), ), + getSessionReplayProperties: debugWrapper( + sessionReplay.getSessionReplayProperties.bind(sessionReplay), + 'getSessionReplayProperties', + getLogConfig(sessionReplay), + ), shutdown: debugWrapper(sessionReplay.shutdown.bind(sessionReplay), 'teardown', getLogConfig(sessionReplay)), }; }; diff --git a/packages/session-replay-browser/src/typings/session-replay.ts b/packages/session-replay-browser/src/typings/session-replay.ts index e58f49e3c..9a489a2ef 100644 --- a/packages/session-replay-browser/src/typings/session-replay.ts +++ b/packages/session-replay-browser/src/typings/session-replay.ts @@ -47,5 +47,6 @@ export interface AmplitudeSessionReplay { init: (apiKey: string, options: SessionReplayOptions) => AmplitudeReturn; setSessionId: (sessionId: number) => void; getSessionRecordingProperties: () => { [key: string]: boolean }; + getSessionReplayProperties: () => { [key: string]: boolean }; shutdown: () => void; } From 0e2a9ed0a06f4a2e2c3d2d9c1a4cdd57552b495f Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Tue, 12 Sep 2023 16:39:09 +0000 Subject: [PATCH 161/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.6.9 - @amplitude/session-replay-browser@0.2.1 --- packages/plugin-session-replay-browser/CHANGELOG.md | 9 +++++++++ packages/plugin-session-replay-browser/package.json | 4 ++-- packages/session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/session-replay-browser/package.json | 2 +- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index aab7fe0ff..859ee0d95 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.6.9](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.8...@amplitude/plugin-session-replay-browser@0.6.9) (2023-09-12) + +**Note:** Version bump only for package @amplitude/plugin-session-replay-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.6.8](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.7...@amplitude/plugin-session-replay-browser@0.6.8) (2023-09-11) **Note:** Version bump only for package @amplitude/plugin-session-replay-browser diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index f35162552..a025ce0ea 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.6.8", + "version": "0.6.9", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -41,7 +41,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "^0.2.0", + "@amplitude/session-replay-browser": "^0.2.1", "idb-keyval": "^6.2.1", "rrweb": "^2.0.0-alpha.11", "tslib": "^2.4.1" diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index 325078bbf..3676cab50 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.2.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.2.0...@amplitude/session-replay-browser@0.2.1) (2023-09-12) + +### Bug Fixes + +- **session replay:** expose getSessionReplayProperties externally + ([597201a](https://github.com/amplitude/Amplitude-TypeScript/commit/597201a04b5c7c51b3f047caaf39986af6fb6534)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [0.2.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.1.1...@amplitude/session-replay-browser@0.2.0) (2023-09-11) ### Features diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index 510855b49..dc0dae972 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "0.2.0", + "version": "0.2.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From fa72e9bacc6132ef37122f0a63419150f2f690e2 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Tue, 12 Sep 2023 13:14:46 -0400 Subject: [PATCH 162/214] fix(session replay): remove beta tag from package.json to update public facing version --- packages/plugin-session-replay-browser/package.json | 3 +-- packages/session-replay-browser/package.json | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index a025ce0ea..014ceeb24 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -10,8 +10,7 @@ "types": "lib/esm/index.d.ts", "sideEffects": false, "publishConfig": { - "access": "public", - "tag": "beta" + "access": "public" }, "repository": { "type": "git", diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index dc0dae972..40bbbbf54 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -10,8 +10,7 @@ "types": "lib/esm/index.d.ts", "sideEffects": false, "publishConfig": { - "access": "public", - "tag": "beta" + "access": "public" }, "repository": { "type": "git", From 1486b2860019c9c4bac9a4db1a2d660d4a3c107c Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Tue, 12 Sep 2023 17:58:55 +0000 Subject: [PATCH 163/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.6.10 - @amplitude/session-replay-browser@0.2.2 --- packages/plugin-session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/plugin-session-replay-browser/package.json | 4 ++-- packages/session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/session-replay-browser/package.json | 2 +- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 859ee0d95..9db688be0 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.6.10](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.9...@amplitude/plugin-session-replay-browser@0.6.10) (2023-09-12) + +### Bug Fixes + +- **session replay:** remove beta tag from package.json to update public facing version + ([fa72e9b](https://github.com/amplitude/Amplitude-TypeScript/commit/fa72e9bacc6132ef37122f0a63419150f2f690e2)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.6.9](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.8...@amplitude/plugin-session-replay-browser@0.6.9) (2023-09-12) **Note:** Version bump only for package @amplitude/plugin-session-replay-browser diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 014ceeb24..7f4f5d35a 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.6.9", + "version": "0.6.10", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -40,7 +40,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "^0.2.1", + "@amplitude/session-replay-browser": "^0.2.2", "idb-keyval": "^6.2.1", "rrweb": "^2.0.0-alpha.11", "tslib": "^2.4.1" diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index 3676cab50..bd9e77c23 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.2.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.2.1...@amplitude/session-replay-browser@0.2.2) (2023-09-12) + +### Bug Fixes + +- **session replay:** remove beta tag from package.json to update public facing version + ([fa72e9b](https://github.com/amplitude/Amplitude-TypeScript/commit/fa72e9bacc6132ef37122f0a63419150f2f690e2)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.2.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.2.0...@amplitude/session-replay-browser@0.2.1) (2023-09-12) ### Bug Fixes diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index 40bbbbf54..b1191efd5 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "0.2.1", + "version": "0.2.2", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 60df827d56f773d919eba09e1d70132ff3ed6670 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Mon, 18 Sep 2023 11:59:08 -0400 Subject: [PATCH 164/214] fix(session replay): Update endpoint for session replay and only retry for 5xx responses --- .../session-replay-browser/src/constants.ts | 4 ++-- .../session-replay-browser/src/messages.ts | 1 + .../src/session-replay.ts | 6 +++++- .../test/session-replay.test.ts | 20 +++++++++---------- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/session-replay-browser/src/constants.ts b/packages/session-replay-browser/src/constants.ts index 8e02f45a8..3bc79f9ef 100644 --- a/packages/session-replay-browser/src/constants.ts +++ b/packages/session-replay-browser/src/constants.ts @@ -10,8 +10,8 @@ export const DEFAULT_SESSION_END_EVENT = 'session_end'; export const BLOCK_CLASS = 'amp-block'; export const MASK_TEXT_CLASS = 'amp-mask'; export const UNMASK_TEXT_CLASS = 'amp-unmask'; -export const SESSION_REPLAY_SERVER_URL = 'https://api-secure.amplitude.com/sessions/track'; -export const SESSION_REPLAY_EU_URL = 'https://api.eu.amplitude.com/sessions/track'; +export const SESSION_REPLAY_SERVER_URL = 'https://api-sr.amplitude.com/sessions/track'; +export const SESSION_REPLAY_EU_URL = 'https://api-sr.eu.amplitude.com/sessions/track'; export const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 500; // derived by JSON stringifying an example payload without events export const MAX_EVENT_LIST_SIZE_IN_BYTES = 10 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; diff --git a/packages/session-replay-browser/src/messages.ts b/packages/session-replay-browser/src/messages.ts index a264a8fda..1dfa64364 100644 --- a/packages/session-replay-browser/src/messages.ts +++ b/packages/session-replay-browser/src/messages.ts @@ -1,6 +1,7 @@ export const getSuccessMessage = (sessionId: number) => `Session replay event batch tracked successfully for session id ${sessionId}`; export const UNEXPECTED_ERROR_MESSAGE = 'Unexpected error occurred'; +export const UNEXPECTED_NETWORK_ERROR_MESSAGE = 'Network error occurred, event batch rejected'; export const MAX_RETRIES_EXCEEDED_MESSAGE = 'Session replay event batch rejected due to exceeded retry count'; export const STORAGE_FAILURE = 'Failed to store session replay events in IndexedDB'; export const MISSING_DEVICE_ID_MESSAGE = 'Session replay event batch not sent due to missing device ID'; diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index 4dca0f350..aff4fa2be 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -24,6 +24,7 @@ import { MISSING_DEVICE_ID_MESSAGE, STORAGE_FAILURE, UNEXPECTED_ERROR_MESSAGE, + UNEXPECTED_NETWORK_ERROR_MESSAGE, getSuccessMessage, } from './messages'; import { @@ -430,8 +431,11 @@ export class SessionReplay implements AmplitudeSessionReplay { case Status.Success: this.handleSuccessResponse(context); break; - default: + case Status.Failed: this.handleOtherResponse(context); + break; + default: + this.completeRequest({ context, err: UNEXPECTED_NETWORK_ERROR_MESSAGE }); } } diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index d0118c787..918c592b8 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -982,7 +982,7 @@ describe('SessionReplayPlugin', () => { await sessionReplay.send(context); expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch).toHaveBeenCalledWith('https://api-secure.amplitude.com/sessions/track', { + expect(fetch).toHaveBeenCalledWith('https://api-sr.amplitude.com/sessions/track', { body: JSON.stringify({ api_key: 'static_key', device_id: '1a2b3c', @@ -1008,7 +1008,7 @@ describe('SessionReplayPlugin', () => { await sessionReplay.send(context); expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch).toHaveBeenCalledWith('https://api.eu.amplitude.com/sessions/track', { + expect(fetch).toHaveBeenCalledWith('https://api-sr.eu.amplitude.com/sessions/track', { body: JSON.stringify({ api_key: 'static_key', device_id: '1a2b3c', @@ -1177,7 +1177,7 @@ describe('SessionReplayPlugin', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual('API Failure'); }); - test('should handle retry for 400 error', async () => { + test('should not retry for 400 error', async () => { (fetch as jest.Mock) .mockImplementationOnce(() => { return Promise.resolve({ @@ -1197,15 +1197,11 @@ describe('SessionReplayPlugin', () => { sessionReplay.events = [mockEventString]; sessionReplay.stopRecordingAndSendEvents(); await runScheduleTimers(); - expect(fetch).toHaveBeenCalledTimes(2); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(0); + expect(fetch).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.log).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.log.mock.calls[0][0]).toEqual(getSuccessMessage(123)); + expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); }); - test('should handle retry for 413 error', async () => { + test('should not retry for 413 error', async () => { (fetch as jest.Mock) .mockImplementationOnce(() => { return Promise.resolve({ @@ -1222,7 +1218,9 @@ describe('SessionReplayPlugin', () => { sessionReplay.events = [mockEventString]; sessionReplay.stopRecordingAndSendEvents(); await runScheduleTimers(); - expect(fetch).toHaveBeenCalledTimes(2); + expect(fetch).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); }); test('should handle retry for 500 error', async () => { (fetch as jest.Mock) From f30d116fdd253e3d0fca2eeed605cd4cb7806a03 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Mon, 18 Sep 2023 20:28:37 +0000 Subject: [PATCH 165/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.6.11 - @amplitude/session-replay-browser@0.2.3 --- packages/plugin-session-replay-browser/CHANGELOG.md | 9 +++++++++ packages/plugin-session-replay-browser/package.json | 4 ++-- packages/session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/session-replay-browser/package.json | 2 +- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 9db688be0..ad1ae998e 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.6.11](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.10...@amplitude/plugin-session-replay-browser@0.6.11) (2023-09-18) + +**Note:** Version bump only for package @amplitude/plugin-session-replay-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.6.10](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.9...@amplitude/plugin-session-replay-browser@0.6.10) (2023-09-12) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 7f4f5d35a..19e7677e8 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.6.10", + "version": "0.6.11", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -40,7 +40,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "^0.2.2", + "@amplitude/session-replay-browser": "^0.2.3", "idb-keyval": "^6.2.1", "rrweb": "^2.0.0-alpha.11", "tslib": "^2.4.1" diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index bd9e77c23..c5355d4c1 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.2.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.2.2...@amplitude/session-replay-browser@0.2.3) (2023-09-18) + +### Bug Fixes + +- **session replay:** Update endpoint for session replay and only retry for 5xx responses + ([60df827](https://github.com/amplitude/Amplitude-TypeScript/commit/60df827d56f773d919eba09e1d70132ff3ed6670)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.2.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.2.1...@amplitude/session-replay-browser@0.2.2) (2023-09-12) ### Bug Fixes diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index b1191efd5..b01ee8ad7 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "0.2.2", + "version": "0.2.3", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 7b3b571e38a9bf968581cabbff50c6c1fff7251f Mon Sep 17 00:00:00 2001 From: Jesse Wang Date: Wed, 20 Sep 2023 14:35:07 -0700 Subject: [PATCH 166/214] fix(session replay): opt out should take effect immediately --- packages/session-replay-browser/src/session-replay.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index 4dca0f350..724ff2997 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -177,7 +177,7 @@ export class SessionReplay implements AmplitudeSessionReplay { identityStoreOptOut = identityStore.getIdentity().optOut; } - return identityStoreOptOut || this.config?.optOut; + return identityStoreOptOut !== undefined ? identityStoreOptOut : this.config?.optOut; } getShouldRecord() { @@ -241,7 +241,7 @@ export class SessionReplay implements AmplitudeSessionReplay { this.stopRecordingEvents = record({ emit: (event) => { const globalScope = getGlobalScope(); - if (globalScope && globalScope.document && !globalScope.document.hasFocus()) { + if ((globalScope && globalScope.document && !globalScope.document.hasFocus()) || !this.getShouldRecord()) { this.stopRecordingAndSendEvents(); return; } From 0645fb27d53be3735971e3767ae8d722e9a47999 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Wed, 20 Sep 2023 20:32:53 -0400 Subject: [PATCH 167/214] fix(session replay) set max retries to cap at 1 and update to new api endpoint --- packages/session-replay-browser/src/config.ts | 6 +- .../session-replay-browser/src/constants.ts | 7 +- .../src/session-replay.ts | 21 ++--- .../test/session-replay.test.ts | 79 +++++++++++++------ 4 files changed, 76 insertions(+), 37 deletions(-) diff --git a/packages/session-replay-browser/src/config.ts b/packages/session-replay-browser/src/config.ts index 3ebcb55df..4f6482d4e 100644 --- a/packages/session-replay-browser/src/config.ts +++ b/packages/session-replay-browser/src/config.ts @@ -4,7 +4,7 @@ import { LogLevel } from '@amplitude/analytics-types'; import { SessionReplayConfig as ISessionReplayConfig, SessionReplayOptions } from './typings/session-replay'; export const getDefaultConfig = () => ({ - flushMaxRetries: 5, + flushMaxRetries: 2, logLevel: LogLevel.Warn, loggerProvider: new Logger(), transportProvider: new FetchTransport(), @@ -23,6 +23,10 @@ export class SessionReplayConfig extends Config implements ISessionReplayConfig ...options, apiKey, }); + this.flushMaxRetries = + options.flushMaxRetries !== undefined && options.flushMaxRetries <= defaultConfig.flushMaxRetries + ? options.flushMaxRetries + : defaultConfig.flushMaxRetries; this.apiKey = apiKey; this.sampleRate = options.sampleRate || 1; diff --git a/packages/session-replay-browser/src/constants.ts b/packages/session-replay-browser/src/constants.ts index 3bc79f9ef..a1350cff6 100644 --- a/packages/session-replay-browser/src/constants.ts +++ b/packages/session-replay-browser/src/constants.ts @@ -10,11 +10,10 @@ export const DEFAULT_SESSION_END_EVENT = 'session_end'; export const BLOCK_CLASS = 'amp-block'; export const MASK_TEXT_CLASS = 'amp-mask'; export const UNMASK_TEXT_CLASS = 'amp-unmask'; -export const SESSION_REPLAY_SERVER_URL = 'https://api-sr.amplitude.com/sessions/track'; -export const SESSION_REPLAY_EU_URL = 'https://api-sr.eu.amplitude.com/sessions/track'; +export const SESSION_REPLAY_SERVER_URL = 'https://api-sr.amplitude.com/sessions/v2/track'; +export const SESSION_REPLAY_EU_URL = 'https://api-sr.eu.amplitude.com/sessions/v2/track'; export const STORAGE_PREFIX = `${AMPLITUDE_PREFIX}_replay_unsent`; -const PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS = 500; // derived by JSON stringifying an example payload without events -export const MAX_EVENT_LIST_SIZE_IN_BYTES = 10 * 1000000 - PAYLOAD_ESTIMATED_SIZE_IN_BYTES_WITHOUT_EVENTS; +export const MAX_EVENT_LIST_SIZE_IN_BYTES = 1 * 1000000; // 1 MB export const MIN_INTERVAL = 500; // 500 ms export const MAX_INTERVAL = 10 * 1000; // 10 seconds export const defaultSessionStore: IDBStoreSession = { diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index aff4fa2be..a29efbb7e 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -383,27 +383,28 @@ export class SessionReplay implements AmplitudeSessionReplay { if (!deviceId) { return this.completeRequest({ context, err: MISSING_DEVICE_ID_MESSAGE }); } - const payload = { - api_key: apiKey, + + const urlParams = new URLSearchParams({ device_id: deviceId, - session_id: context.sessionId, - start_timestamp: context.sessionId, - events_batch: { - version: 1, - events: context.events, - seq_number: context.sequenceId, - }, + session_id: `${context.sessionId}`, + seq_number: `${context.sequenceId}`, + }); + + const payload = { + version: 1, + events: context.events, }; try { const options: RequestInit = { headers: { 'Content-Type': 'application/json', Accept: '*/*', + Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify(payload), method: 'POST', }; - const server_url = this.getServerUrl(); + const server_url = `${this.getServerUrl()}?${urlParams.toString()}`; const res = await fetch(server_url, options); if (res === null) { this.completeRequest({ context, err: UNEXPECTED_ERROR_MESSAGE }); diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index 918c592b8..c7ffb3295 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -142,6 +142,27 @@ describe('SessionReplayPlugin', () => { } as typeof globalThis); expect(initialize).not.toHaveBeenCalled(); }); + + describe('flushMaxRetries config', () => { + test('should use default config value if no max retries', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { ...mockOptions, flushMaxRetries: undefined }).promise; + + expect(sessionReplay.config?.flushMaxRetries).toBe(2); + }); + test('should cap max retries at default config value', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { ...mockOptions, flushMaxRetries: 10 }).promise; + + expect(sessionReplay.config?.flushMaxRetries).toBe(2); + }); + test('should allow a lower value than default config value', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { ...mockOptions, flushMaxRetries: 0 }).promise; + + expect(sessionReplay.config?.flushMaxRetries).toBe(0); + }); + }); }); describe('setSessionId', () => { @@ -982,17 +1003,14 @@ describe('SessionReplayPlugin', () => { await sessionReplay.send(context); expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch).toHaveBeenCalledWith('https://api-sr.amplitude.com/sessions/track', { - body: JSON.stringify({ - api_key: 'static_key', - device_id: '1a2b3c', - session_id: 123, - start_timestamp: 123, - events_batch: { version: 1, events: [mockEventString], seq_number: 1 }, - }), - headers: { Accept: '*/*', 'Content-Type': 'application/json' }, - method: 'POST', - }); + expect(fetch).toHaveBeenCalledWith( + 'https://api-sr.amplitude.com/sessions/v2/track?device_id=1a2b3c&session_id=123&seq_number=1', + { + body: JSON.stringify({ version: 1, events: [mockEventString] }), + headers: { Accept: '*/*', 'Content-Type': 'application/json', Authorization: 'Bearer static_key' }, + method: 'POST', + }, + ); }); test('should make a request to eu', async () => { const sessionReplay = new SessionReplay(); @@ -1008,17 +1026,14 @@ describe('SessionReplayPlugin', () => { await sessionReplay.send(context); expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch).toHaveBeenCalledWith('https://api-sr.eu.amplitude.com/sessions/track', { - body: JSON.stringify({ - api_key: 'static_key', - device_id: '1a2b3c', - session_id: 123, - start_timestamp: 123, - events_batch: { version: 1, events: [mockEventString], seq_number: 1 }, - }), - headers: { Accept: '*/*', 'Content-Type': 'application/json' }, - method: 'POST', - }); + expect(fetch).toHaveBeenCalledWith( + 'https://api-sr.eu.amplitude.com/sessions/v2/track?device_id=1a2b3c&session_id=123&seq_number=1', + { + body: JSON.stringify({ version: 1, events: [mockEventString] }), + headers: { Accept: '*/*', 'Content-Type': 'application/json', Authorization: 'Bearer static_key' }, + method: 'POST', + }, + ); }); test('should update IDB store upon success', async () => { const sessionReplay = new SessionReplay(); @@ -1241,6 +1256,26 @@ describe('SessionReplayPlugin', () => { await runScheduleTimers(); expect(fetch).toHaveBeenCalledTimes(2); }); + + test('should only retry once for 500 error, even if config set to higher than one retry', async () => { + (fetch as jest.Mock) + .mockImplementationOnce(() => { + return Promise.resolve({ + status: 500, + }); + }) + .mockImplementationOnce(() => { + return Promise.resolve({ + status: 500, + }); + }); + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { ...mockOptions, flushMaxRetries: 10 }).promise; + sessionReplay.events = [mockEventString]; + sessionReplay.stopRecordingAndSendEvents(); + await runScheduleTimers(); + expect(fetch).toHaveBeenCalledTimes(2); + }); test('should handle retry for 503 error', async () => { (fetch as jest.Mock) .mockImplementationOnce(() => { From be0375ac85cd57b6cb5fedfcb39625c7631350bc Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 21 Sep 2023 14:11:43 +0000 Subject: [PATCH 168/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.6.12 - @amplitude/session-replay-browser@0.2.4 --- packages/plugin-session-replay-browser/CHANGELOG.md | 9 +++++++++ packages/plugin-session-replay-browser/package.json | 4 ++-- packages/session-replay-browser/CHANGELOG.md | 9 +++++++++ packages/session-replay-browser/package.json | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index ad1ae998e..d3559b1ed 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.6.12](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.11...@amplitude/plugin-session-replay-browser@0.6.12) (2023-09-21) + +**Note:** Version bump only for package @amplitude/plugin-session-replay-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.6.11](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.10...@amplitude/plugin-session-replay-browser@0.6.11) (2023-09-18) **Note:** Version bump only for package @amplitude/plugin-session-replay-browser diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 19e7677e8..5044edb48 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.6.11", + "version": "0.6.12", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -40,7 +40,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "^0.2.3", + "@amplitude/session-replay-browser": "^0.2.4", "idb-keyval": "^6.2.1", "rrweb": "^2.0.0-alpha.11", "tslib": "^2.4.1" diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index c5355d4c1..a9eacf09b 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.2.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.2.3...@amplitude/session-replay-browser@0.2.4) (2023-09-21) + +**Note:** Version bump only for package @amplitude/session-replay-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.2.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.2.2...@amplitude/session-replay-browser@0.2.3) (2023-09-18) ### Bug Fixes diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index b01ee8ad7..d4ea166d7 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "0.2.3", + "version": "0.2.4", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 7330d4912c719804a3a5082d6b75a976d68cbda0 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Thu, 21 Sep 2023 10:49:47 -0400 Subject: [PATCH 169/214] fix(session replay plugin): update plugin to use the sessionReplayProperties fn --- packages/plugin-session-replay-browser/src/session-replay.ts | 2 +- .../plugin-session-replay-browser/test/session-replay.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 151a50c38..10d246b1d 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -54,7 +54,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { sessionReplay.setSessionId(event.session_id); } - const sessionRecordingProperties = sessionReplay.getSessionRecordingProperties(); + const sessionRecordingProperties = sessionReplay.getSessionReplayProperties(); event.event_properties = { ...event.event_properties, ...sessionRecordingProperties, diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 2232b18fb..f97fa6cf8 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -8,7 +8,7 @@ type MockedSessionReplayBrowser = jest.Mocked; describe('SessionReplayPlugin', () => { - const { init, setSessionId, getSessionRecordingProperties, shutdown } = + const { init, setSessionId, getSessionReplayProperties, shutdown } = sessionReplayBrowser as MockedSessionReplayBrowser; const mockLoggerProvider: MockedLogger = { error: jest.fn(), @@ -127,7 +127,7 @@ describe('SessionReplayPlugin', () => { test('should add event property for [Amplitude] Session Recorded', async () => { const sessionReplay = sessionReplayPlugin(); await sessionReplay.setup(mockConfig); - getSessionRecordingProperties.mockReturnValueOnce({ + getSessionReplayProperties.mockReturnValueOnce({ '[Amplitude] Session Recorded': true, }); const event = { From 18034a87d78887f8bf1bee41dbb8bea201a6f522 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 21 Sep 2023 16:09:57 +0000 Subject: [PATCH 170/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.6.13 --- packages/plugin-session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/plugin-session-replay-browser/package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index d3559b1ed..b4427c7b2 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.6.13](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.12...@amplitude/plugin-session-replay-browser@0.6.13) (2023-09-21) + +### Bug Fixes + +- **session replay plugin:** update plugin to use the sessionReplayProperties fn + ([7330d49](https://github.com/amplitude/Amplitude-TypeScript/commit/7330d4912c719804a3a5082d6b75a976d68cbda0)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.6.12](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.11...@amplitude/plugin-session-replay-browser@0.6.12) (2023-09-21) **Note:** Version bump only for package @amplitude/plugin-session-replay-browser diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 5044edb48..a43358fb0 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.6.12", + "version": "0.6.13", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 938b5d524780385bff04d5a51588407712d6db66 Mon Sep 17 00:00:00 2001 From: Jesse Wang Date: Thu, 21 Sep 2023 10:26:45 -0700 Subject: [PATCH 171/214] fix(session replay): test identity store false case and opt out in middle of recording --- .../test/session-replay.test.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index d0118c787..1eb682e69 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -524,6 +524,20 @@ describe('SessionReplayPlugin', () => { await sessionReplay.init(apiKey, { ...mockOptions, instanceName: 'my_instance' }).promise; expect(sessionReplay.shouldOptOut()).toEqual(true); }); + test('should return opt out from identity store even if set to false', async () => { + jest.spyOn(AnalyticsClientCommon, 'getAnalyticsConnector').mockReturnValue({ + identityStore: { + getIdentity: () => { + return { + optOut: false, + }; + }, + }, + } as unknown as ReturnType); + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { ...mockOptions, instanceName: 'my_instance', optOut: true }).promise; + expect(sessionReplay.shouldOptOut()).toEqual(false); + }); test('should return config device id if set', async () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, { ...mockOptions, instanceName: 'my_instance', optOut: true }).promise; @@ -652,6 +666,15 @@ describe('SessionReplayPlugin', () => { expect(record).not.toHaveBeenCalled(); expect(sessionReplay.events).toEqual([]); }); + + test('should return early if user opts out', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { ...mockOptions, optOut: true }).promise; + sessionReplay.recordEvents(); + expect(record).not.toHaveBeenCalled(); + expect(sessionReplay.events).toEqual([]); + }); + test('should store events in class and in IDB', async () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, mockOptions).promise; @@ -763,6 +786,30 @@ describe('SessionReplayPlugin', () => { expect(sessionReplay.events).toEqual([mockEventString]); // events should not change, emmitted event should be ignored }); + test('should stop recording and send events if user opts out during recording', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, mockOptions).promise; + sessionReplay.recordEvents(); + const stopRecordingMock = jest.fn(); + sessionReplay.stopRecordingEvents = stopRecordingMock; + expect(sessionReplay.events).toEqual([]); + sessionReplay.events = [mockEventString]; // Add one event to list to trigger sending in stopRecordingAndSendEvents + // eslint-disable-next-line @typescript-eslint/no-empty-function + const sendEventsListMock = jest.spyOn(sessionReplay, 'sendEventsList').mockImplementationOnce(() => {}); + sessionReplay.shouldOptOut = () => true; + const recordArg = record.mock.calls[0][0]; + recordArg?.emit && recordArg?.emit(mockEvent); + expect(sendEventsListMock).toHaveBeenCalledTimes(1); + expect(sendEventsListMock).toHaveBeenCalledWith({ + events: [mockEventString], + sequenceId: 0, + sessionId: 123, + }); + expect(stopRecordingMock).toHaveBeenCalled(); + expect(sessionReplay.stopRecordingEvents).toEqual(null); + expect(sessionReplay.events).toEqual([mockEventString]); // events should not change, emmitted event should be ignored + }); + test('should add an error handler', async () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, mockOptions).promise; From 5af910adb568057c875ad97c9eb50a3d0eae8876 Mon Sep 17 00:00:00 2001 From: Marvin Liu Date: Fri, 22 Sep 2023 14:29:34 -0700 Subject: [PATCH 172/214] build: temp disable auto-tracking package publish (#590) --- packages/plugin-auto-tracking-browser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-auto-tracking-browser/package.json b/packages/plugin-auto-tracking-browser/package.json index 4553e32cd..45ea19e85 100644 --- a/packages/plugin-auto-tracking-browser/package.json +++ b/packages/plugin-auto-tracking-browser/package.json @@ -10,9 +10,9 @@ "types": "lib/esm/index.d.ts", "sideEffects": false, "publishConfig": { - "access": "public", "tag": "latest" }, + "private": true, "repository": { "type": "git", "url": "git+https://github.com/amplitude/Amplitude-TypeScript.git" From c5907ef345cb1986364a62a2ee6f4b34cb96ccff Mon Sep 17 00:00:00 2001 From: Justin Fiedler Date: Tue, 26 Sep 2023 17:42:25 -0700 Subject: [PATCH 173/214] fix: Amp 85112 set user id is not queued in snippet v1 (#592) * fix: proxy non-promised methods in browser-snippet (#591) * fix: proxy non-promised methods in browser-snippet * fix: removed extra semicolon in browser-snippet template * fix: revert manual generated file changes * chore: undo manual changes to generated snippet * fix: update README.md for lerna to pickup package changes --- packages/analytics-browser/README.md | 2 +- scripts/templates/browser-snippet.template.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/analytics-browser/README.md b/packages/analytics-browser/README.md index 30aa003fc..122abfd76 100644 --- a/packages/analytics-browser/README.md +++ b/packages/analytics-browser/README.md @@ -11,7 +11,7 @@ Official Amplitude SDK for Web # Doc -See our [Typescript Analytics Browser SDK](https://amplitude.github.io/Amplitude-TypeScript/modules/_amplitude_analytics_browser.html) Reference for a list and description of all available SDK methods. +See our [Analytics SDK for Browser](https://amplitude.github.io/Amplitude-TypeScript/modules/_amplitude_analytics_browser.html) Reference for a list and description of all available SDK methods. # Installation and Quick Start diff --git a/scripts/templates/browser-snippet.template.js b/scripts/templates/browser-snippet.template.js index cb6211e32..237a81d98 100644 --- a/scripts/templates/browser-snippet.template.js +++ b/scripts/templates/browser-snippet.template.js @@ -96,12 +96,19 @@ const snippet = (name, integrity, version, globalVar) => ` }); }; } + function proxyInstance(instance, fn, args) { + instance._q.push({ + name: fn, + args: Array.prototype.slice.call(args, 0), + }); + } function proxyMain(instance, fn, isPromise) { var args = arguments; instance[fn] = function () { if (isPromise) return { promise: new Promise(getPromiseResult(instance, fn, Array.prototype.slice.call(arguments))), }; + proxyInstance(instance, fn, Array.prototype.slice.call(arguments)); }; } function setUpProxy(instance) { From 0fd37e7445603b0a8e28e6d441aefed008d4845a Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Wed, 27 Sep 2023 01:00:44 +0000 Subject: [PATCH 174/214] chore(release): publish - @amplitude/analytics-browser-test@1.4.11 - @amplitude/analytics-browser@1.13.1 - @amplitude/marketing-analytics-browser@1.0.11 - @amplitude/plugin-auto-tracking-browser@0.1.2 - @amplitude/plugin-session-replay-browser@0.6.14 - @amplitude/session-replay-browser@0.2.5 --- packages/analytics-browser-test/CHANGELOG.md | 9 +++++++++ packages/analytics-browser-test/package.json | 4 ++-- packages/analytics-browser/CHANGELOG.md | 14 ++++++++++++++ packages/analytics-browser/README.md | 2 +- .../generated/amplitude-snippet.js | 11 +++++++++-- packages/analytics-browser/package.json | 2 +- packages/analytics-browser/playground/amplitude.js | 2 +- packages/analytics-browser/src/version.ts | 2 +- packages/marketing-analytics-browser/CHANGELOG.md | 9 +++++++++ packages/marketing-analytics-browser/README.md | 2 +- .../generated/amplitude-gtm-snippet.js | 11 +++++++++-- .../generated/amplitude-snippet.js | 11 +++++++++-- packages/marketing-analytics-browser/package.json | 4 ++-- .../marketing-analytics-browser/src/version.ts | 2 +- packages/plugin-auto-tracking-browser/CHANGELOG.md | 9 +++++++++ packages/plugin-auto-tracking-browser/package.json | 2 +- .../plugin-session-replay-browser/CHANGELOG.md | 9 +++++++++ .../plugin-session-replay-browser/package.json | 4 ++-- packages/session-replay-browser/CHANGELOG.md | 14 ++++++++++++++ packages/session-replay-browser/package.json | 2 +- 20 files changed, 105 insertions(+), 20 deletions(-) diff --git a/packages/analytics-browser-test/CHANGELOG.md b/packages/analytics-browser-test/CHANGELOG.md index 623bfaa9b..55639540d 100644 --- a/packages/analytics-browser-test/CHANGELOG.md +++ b/packages/analytics-browser-test/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.4.11](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.10...@amplitude/analytics-browser-test@1.4.11) (2023-09-27) + +**Note:** Version bump only for package @amplitude/analytics-browser-test + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.4.10](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.9...@amplitude/analytics-browser-test@1.4.10) (2023-08-22) **Note:** Version bump only for package @amplitude/analytics-browser-test diff --git a/packages/analytics-browser-test/package.json b/packages/analytics-browser-test/package.json index a06ddbe2b..8844904a9 100644 --- a/packages/analytics-browser-test/package.json +++ b/packages/analytics-browser-test/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser-test", - "version": "1.4.10", + "version": "1.4.11", "private": true, "description": "", "author": "Amplitude Inc", @@ -16,7 +16,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.13.0" + "@amplitude/analytics-browser": "^1.13.1" }, "devDependencies": { "nock": "^13.2.4" diff --git a/packages/analytics-browser/CHANGELOG.md b/packages/analytics-browser/CHANGELOG.md index d8ff4a4bf..bee607e61 100644 --- a/packages/analytics-browser/CHANGELOG.md +++ b/packages/analytics-browser/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.13.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.13.0...@amplitude/analytics-browser@1.13.1) (2023-09-27) + +### Bug Fixes + +- Amp 85112 set user id is not queued in snippet v1 + ([#592](https://github.com/amplitude/Amplitude-TypeScript/issues/592)) + ([c5907ef](https://github.com/amplitude/Amplitude-TypeScript/commit/c5907ef345cb1986364a62a2ee6f4b34cb96ccff)), closes + [#591](https://github.com/amplitude/Amplitude-TypeScript/issues/591) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [1.13.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.12.3...@amplitude/analytics-browser@1.13.0) (2023-08-22) ### Features diff --git a/packages/analytics-browser/README.md b/packages/analytics-browser/README.md index 122abfd76..888a9cbf8 100644 --- a/packages/analytics-browser/README.md +++ b/packages/analytics-browser/README.md @@ -40,7 +40,7 @@ Alternatively, the package is also distributed through a CDN. Copy and paste the ```html diff --git a/packages/analytics-browser/generated/amplitude-snippet.js b/packages/analytics-browser/generated/amplitude-snippet.js index 52412ae4c..13541e4f0 100644 --- a/packages/analytics-browser/generated/amplitude-snippet.js +++ b/packages/analytics-browser/generated/amplitude-snippet.js @@ -30,12 +30,19 @@ }); }; }; + var proxyInstance = function proxyInstance(instance, fn, args) { + instance._q.push({ + name: fn, + args: Array.prototype.slice.call(args, 0) + }); + }; var proxyMain = function proxyMain(instance, fn, isPromise) { var args = arguments; instance[fn] = function () { if (isPromise) return { promise: new Promise(getPromiseResult(instance, fn, Array.prototype.slice.call(arguments))) }; + proxyInstance(instance, fn, Array.prototype.slice.call(arguments)); }; }; var setUpProxy = function setUpProxy(instance) { @@ -49,10 +56,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-TC+p239y8CXxMMM59C+TfQA+0r+7U8eQii58L5ca0AMJocaCOj9mvu4YZsdteSQz'; + as.integrity = 'sha384-uxjIeuRBYErRNi6HlbBflpiMqMt+LcL2LeZfCcStRQhWbSNrCMi0C+nvW8npPWSt'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.13.0-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.13.1-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/analytics-browser/package.json b/packages/analytics-browser/package.json index c083bff0a..e34e84baf 100644 --- a/packages/analytics-browser/package.json +++ b/packages/analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser", - "version": "1.13.0", + "version": "1.13.1", "description": "Official Amplitude SDK for Web", "keywords": [ "analytics", diff --git a/packages/analytics-browser/playground/amplitude.js b/packages/analytics-browser/playground/amplitude.js index c01ac48de..c146ddd07 100644 --- a/packages/analytics-browser/playground/amplitude.js +++ b/packages/analytics-browser/playground/amplitude.js @@ -1 +1 @@ -!function(){"use strict";var e=function(){function e(){}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:t(),platform:"Web",os:void 0,deviceModel:void 0}},e}(),t=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},i=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),n=function(){return n=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function g(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function b(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!H(t,i))return!1}return!0},H=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=h(t),s=o.next();!s.done;s=o.next()){var a=s.value;if(Array.isArray(a))return!1;if("object"==typeof a)r=r&&K(a);else if(!["number","string"].includes(typeof a))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{s&&!s.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return K(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},W=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return d({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(y.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(y.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(y.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(y.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(y.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(y.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(y.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(y.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(y.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[y.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[y.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===y.ADD?"number"==typeof i:e===y.UNSET||e===y.REMOVE||H(t,i)))},e}(),Z=function(e,t){return d(d({},t),{event_type:w.IDENTIFY,user_properties:e.getUserProperties()})},G=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=E.Unknown),{event:e,code:t,message:i}},J=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return f(this,void 0,void 0,(function(){return v(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){var t;return f(this,void 0,void 0,(function(){var i,n;return v(this,(function(r){switch(r.label){case 0:return i=this.plugins.findIndex((function(t){return t.name===e})),n=this.plugins[i],this.plugins.splice(i,1),[4,null===(t=n.teardown)||void 0===t?void 0:t.call(n)];case 1:return r.sent(),[2]}}))}))},e.prototype.reset=function(e){this.applying=!1,this.plugins.map((function(e){var t;return null===(t=e.teardown)||void 0===t?void 0:t.call(e)})),this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return f(this,void 0,void 0,(function(){var t,i,n,r,o,s,a,u,c,l,p,f,b,y,m,w,_,k,E;return v(this,(function(v){switch(v.label){case 0:if(!e)return[2];t=g(e,1),i=t[0],n=g(e,2),r=n[1],o=this.plugins.filter((function(e){return e.type===I.BEFORE})),v.label=1;case 1:v.trys.push([1,6,7,8]),s=h(o),a=s.next(),v.label=2;case 2:return a.done?[3,5]:[4,a.value.execute(d({},i))];case 3:if(null===(f=v.sent()))return r({event:i,code:0,message:""}),[2];i=f,v.label=4;case 4:return a=s.next(),[3,2];case 5:return[3,8];case 6:return u=v.sent(),w={error:u},[3,8];case 7:try{a&&!a.done&&(_=s.return)&&_.call(s)}finally{if(w)throw w.error}return[7];case 8:c=this.plugins.filter((function(e){return e.type===I.ENRICHMENT})),v.label=9;case 9:v.trys.push([9,14,15,16]),l=h(c),p=l.next(),v.label=10;case 10:return p.done?[3,13]:[4,p.value.execute(d({},i))];case 11:if(null===(f=v.sent()))return r({event:i,code:0,message:""}),[2];i=f,v.label=12;case 12:return p=l.next(),[3,10];case 13:return[3,16];case 14:return b=v.sent(),k={error:b},[3,16];case 15:try{p&&!p.done&&(E=l.return)&&E.call(l)}finally{if(k)throw k.error}return[7];case 16:return y=this.plugins.filter((function(e){return e.type===I.DESTINATION})),m=y.map((function(e){var t=d({},i);return e.execute(t).catch((function(e){return G(t,0,String(e))}))})),Promise.all(m).then((function(e){var t=g(e,1)[0];r(t)})),[2]}}))}))},e.prototype.flush=function(){return f(this,void 0,void 0,(function(){var e,t,i,n=this;return v(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===I.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),Y="Event rejected due to exceeded retry count",X=function(e){return{promise:e||Promise.resolve()}},ee=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new J(this),this.name=e}return e.prototype._init=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return f(this,void 0,void 0,(function(){var t,i,n,r,o,s;return v(this,(function(a){switch(a.label){case 0:t=this[e],this[e]=[],a.label=1;case 1:a.trys.push([1,6,7,8]),i=h(t),n=i.next(),a.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:a.sent(),a.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=a.sent(),o={error:r},[3,8];case 7:try{n&&!n.done&&(s=i.return)&&s.call(i)}finally{if(o)throw o.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,i){var n=function(e,t,i){return d(d(d({},"string"==typeof e?{event_type:e}:e),i),t&&{event_properties:t})}(e,t,i);return X(this.dispatch(n))},e.prototype.identify=function(e,t){var i=Z(e,t);return X(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,i,n){var r=function(e,t,i,n){var r;return d(d({},n),{event_type:w.GROUP_IDENTIFY,group_properties:i.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,i,n);return X(this.dispatch(r))},e.prototype.setGroup=function(e,t,i){var n=function(e,t,i){var n,r=new W;return r.set(e,t),d(d({},i),{event_type:w.IDENTIFY,user_properties:r.getUserProperties(),groups:(n={},n[e]=t,n)})}(e,t,i);return X(this.dispatch(n))},e.prototype.revenue=function(e,t){var i=function(e,t){return d(d({},t),{event_type:w.REVENUE,event_properties:e.getEventProperties()})}(e,t);return X(this.dispatch(i))},e.prototype.add=function(e){return this.config?X(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),X())},e.prototype.remove=function(e){return this.config?X(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),X())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(G(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return f(this,void 0,void 0,(function(){var t=this;return v(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return f(this,void 0,void 0,(function(){var t,i,n;return v(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,G(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=G(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return X(this.timeline.flush())},e}(),te=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return K(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?d({},this.properties):{};return e[m.REVENUE_PRODUCT_ID]=this.productId,e[m.REVENUE_QUANTITY]=this.quantity,e[m.REVENUE_PRICE]=this.price,e[m.REVENUE_TYPE]=this.revenueType,e[m.REVENUE]=this.revenue,e},e}(),ie="Amplitude Logger ",ne=function(){function e(){this.logLevel=_.None}return e.prototype.disable=function(){this.logLevel=_.None},e.prototype.enable=function(e){void 0===e&&(e=_.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),f(this,void 0,void 0,(function(){var t,i,n,r=this;return v(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),f(this,void 0,void 0,(function(){var i,n,r,o,s;return v(this,(function(a){switch(a.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,p(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},a.label=1;case 1:return a.trys.push([1,3,,4]),n=ae(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(n,i)];case 2:return null===(r=a.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(r,e),[3,4]):("body"in r?this.fulfillRequest(e,r.statusCode,"".concat(r.status,": ").concat(ue(r))):this.fulfillRequest(e,r.statusCode,r.status),[2]);case 3:return o=a.sent(),s=(u=o)instanceof Error?u.message:String(u),this.config.loggerProvider.error(s),this.fulfillRequest(e,0,s),[3,4];case 4:return[2]}var u}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case E.Success:this.handleSuccessResponse(e,t);break;case E.Invalid:this.handleInvalidResponse(e,t);break;case E.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case E.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=b(b(b(b([],g(Object.values(e.body.eventsWithInvalidFields)),!1),g(Object.values(e.body.eventsWithMissingFields)),!1),g(Object.values(e.body.eventsWithInvalidIdLengths)),!1),g(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(ue(e)),this.addToQueue.apply(this,b([],g(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(ue(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,b([],g(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),a=new Set(r),u=new Set(o),c=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&a.has(t.event.device_id)))return u.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));c.length>0&&this.config.loggerProvider.warn(ue(e)),this.addToQueue.apply(this,b([],g(c),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,b([],g(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(G(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),pe=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},fe=function(e){return function(){var t=d({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},ve=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=h(t.split(".")),o=r.next();!o.done;o=r.next()){var s=o.value;if(!(s in e))return;e=e[s]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},he=function(e,t){return function(){var i,n,r={};try{for(var o=h(t),s=o.next();!s.done;s=o.next()){var a=s.value;r[a]=ve(e,a)}}catch(e){i={error:e}}finally{try{s&&!s.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ge=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,be)},ye=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return f(this,void 0,void 0,(function(){return v(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return f(this,void 0,void 0,(function(){var t;return v(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return f(this,void 0,void 0,(function(){return v(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return f(this,void 0,void 0,(function(){return v(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),me=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,h,g,b,y,m,w,_,I;if("object"!=typeof e)return null;var k=e.code||0,S=this.buildStatus(k);switch(S){case E.Success:return{status:S,statusCode:k,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case E.Invalid:return{status:S,statusCode:k,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case E.PayloadTooLarge:return{status:S,statusCode:k,body:{error:null!==(h=e.error)&&void 0!==h?h:""}};case E.RateLimit:return{status:S,statusCode:k,body:{error:null!==(g=e.error)&&void 0!==g?g:"",epsThreshold:null!==(b=e.eps_threshold)&&void 0!==b?b:0,throttledDevices:null!==(y=e.throttled_devices)&&void 0!==y?y:{},throttledUsers:null!==(m=e.throttled_users)&&void 0!==m?m:{},exceededDailyQuotaDevices:null!==(w=e.exceeded_daily_quota_devices)&&void 0!==w?w:{},exceededDailyQuotaUsers:null!==(_=e.exceeded_daily_quota_users)&&void 0!==_?_:{},throttledEvents:null!==(I=e.throttled_events)&&void 0!==I?I:[]}};case E.Timeout:default:return{status:S,statusCode:k}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?E.Success:429===e?E.RateLimit:413===e?E.PayloadTooLarge:408===e?E.Timeout:e>=400&&e<500?E.Invalid:e>=500?E.Failed:E.Unknown},e}(),we=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[B,t,e.substring(0,i)].filter(Boolean).join("_")},_e=function(){var e,t,i,n;if("undefined"==typeof navigator)return"";var r=navigator.userLanguage;return null!==(n=null!==(i=null!==(t=null===(e=navigator.languages)||void 0===e?void 0:e[0])&&void 0!==t?t:navigator.language)&&void 0!==i?i:r)&&void 0!==n?n:""},Ie=function(){function e(){this.name="identity",this.type=I.BEFORE,this.identityStore=u().identityStore}return e.prototype.execute=function(e){return f(this,void 0,void 0,(function(){var t;return v(this,(function(i){return(t=e.user_properties)&&this.identityStore.editIdentity().updateUserProperties(t).commit(),[2,e]}))}))},e.prototype.setup=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){return e.instanceName&&(this.identityStore=u(e.instanceName).identityStore),[2]}))}))},e}(),ke=function(){function e(e){this.options=d({},e)}return e.prototype.isEnabled=function(){return f(this,void 0,void 0,(function(){var t,i;return v(this,(function(n){switch(n.label){case 0:if(!O())return[2,!1];e.testValue=String(Date.now()),t=new e(this.options),i="AMP_TEST",n.label=1;case 1:return n.trys.push([1,4,5,7]),[4,t.set(i,e.testValue)];case 2:return n.sent(),[4,t.get(i)];case 3:return[2,n.sent()===e.testValue];case 4:return n.sent(),[2,!1];case 5:return[4,t.remove(i)];case 6:return n.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return f(this,void 0,void 0,(function(){var t;return v(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return f(this,void 0,void 0,(function(){var i,n,r;return v(this,(function(o){return i=O(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return f(this,void 0,void 0,(function(){var n,r,o,s,a,u;return v(this,(function(c){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,o=void 0,(r=null!==t?n:-1)&&((s=new Date).setTime(s.getTime()+24*r*60*60*1e3),o=s),a="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),o&&(a+="; expires=".concat(o.toUTCString())),a+="; path=/",this.options.domain&&(a+="; domain=".concat(this.options.domain)),this.options.secure&&(a+="; Secure"),this.options.sameSite&&(a+="; SameSite=".concat(this.options.sameSite)),(u=O())&&(u.document.cookie=a)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return f(this,void 0,void 0,(function(){return v(this,(function(e){return[2]}))}))},e}(),Ee=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return l(t,e),t.prototype.send=function(e,t){return f(this,void 0,void 0,(function(){var i,n;return v(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},t}(me),Se=function(e){var t={};for(var i in e){var n=e[i];n&&(t[i]=n)}return t},Oe=function(){for(var e,t=[],i=0;iDe;try{o=r?JSON.stringify(t.slice(0,De)):JSON.stringify(t),null===(i=O())||void 0===i||i.localStorage.setItem(e,o)}catch(e){}return r&&(s=t.length-De,null===(n=this.loggerProvider)||void 0===n||n.error("Failed to save ".concat(s," events because the queue length exceeded ").concat(De,"."))),[2]}))}))},e.prototype.remove=function(e){var t;return f(this,void 0,void 0,(function(){return v(this,(function(i){try{null===(t=O())||void 0===t||t.localStorage.removeItem(e)}catch(e){}return[2]}))}))},e.prototype.reset=function(){var e;return f(this,void 0,void 0,(function(){return v(this,(function(t){try{null===(e=O())||void 0===e||e.localStorage.clear()}catch(e){}return[2]}))}))},e}(),Ce=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.state={done:4},t}return l(t,e),t.prototype.send=function(e,t){return f(this,void 0,void 0,(function(){var i=this;return v(this,(function(n){return[2,new Promise((function(n,r){"undefined"==typeof XMLHttpRequest&&r(new Error("XHRTransport is not supported."));var o=new XMLHttpRequest;o.open("POST",e,!0),o.onreadystatechange=function(){if(o.readyState===i.state.done)try{var e=o.responseText,t=JSON.parse(e),s=i.buildResponse(t);n(s)}catch(e){r(e)}},o.setRequestHeader("Content-Type","application/json"),o.setRequestHeader("Accept","*/*"),o.send(JSON.stringify(t))}))]}))}))},t}(me),je=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return l(t,e),t.prototype.send=function(e,t){return f(this,void 0,void 0,(function(){var i=this;return v(this,(function(n){return[2,new Promise((function(n,r){var o=O();if(!(null==o?void 0:o.navigator.sendBeacon))throw new Error("SendBeaconTransport is not supported");try{var s=JSON.stringify(t);return n(o.navigator.sendBeacon(e,JSON.stringify(t))?i.buildResponse({code:200,events_ingested:t.events.length,payload_size_bytes:s.length,server_upload_time:Date.now()}):i.buildResponse({code:500}))}catch(e){r(e)}}))]}))}))},t}(me),Le=function(){return{cookieExpiration:365,cookieSameSite:"Lax",cookieSecure:!1,cookieStorage:new ye,cookieUpgrade:!0,disableCookies:!1,domain:"",sessionTimeout:18e5,trackingOptions:{deviceManufacturer:!0,deviceModel:!0,ipAddress:!0,language:!0,osName:!0,osVersion:!0,platform:!0},transportProvider:new Ee}},Me=function(e){function t(t,i){var n,r,o,s,a,u,c,l,p,f=this,v=Le();return(f=e.call(this,d(d({flushIntervalMillis:1e3,flushMaxRetries:5,flushQueueSize:30,transportProvider:v.transportProvider},i),{apiKey:t}))||this)._optOut=!1,f.cookieStorage=null!==(n=null==i?void 0:i.cookieStorage)&&void 0!==n?n:v.cookieStorage,f.deviceId=null==i?void 0:i.deviceId,f.lastEventId=null==i?void 0:i.lastEventId,f.lastEventTime=null==i?void 0:i.lastEventTime,f.optOut=Boolean(null==i?void 0:i.optOut),f.sessionId=null==i?void 0:i.sessionId,f.userId=null==i?void 0:i.userId,f.appVersion=null==i?void 0:i.appVersion,f.attribution=null==i?void 0:i.attribution,f.cookieExpiration=null!==(r=null==i?void 0:i.cookieExpiration)&&void 0!==r?r:v.cookieExpiration,f.cookieSameSite=null!==(o=null==i?void 0:i.cookieSameSite)&&void 0!==o?o:v.cookieSameSite,f.cookieSecure=null!==(s=null==i?void 0:i.cookieSecure)&&void 0!==s?s:v.cookieSecure,f.cookieUpgrade=null!==(a=null==i?void 0:i.cookieUpgrade)&&void 0!==a?a:v.cookieUpgrade,f.defaultTracking=null==i?void 0:i.defaultTracking,f.disableCookies=null!==(u=null==i?void 0:i.disableCookies)&&void 0!==u?u:v.disableCookies,f.defaultTracking=null==i?void 0:i.defaultTracking,f.domain=null!==(c=null==i?void 0:i.domain)&&void 0!==c?c:v.domain,f.partnerId=null==i?void 0:i.partnerId,f.sessionTimeout=null!==(l=null==i?void 0:i.sessionTimeout)&&void 0!==l?l:v.sessionTimeout,f.trackingOptions=null!==(p=null==i?void 0:i.trackingOptions)&&void 0!==p?p:v.trackingOptions,f}return l(t,e),Object.defineProperty(t.prototype,"deviceId",{get:function(){return this._deviceId},set:function(e){this._deviceId!==e&&(this._deviceId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"userId",{get:function(){return this._userId},set:function(e){this._userId!==e&&(this._userId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"sessionId",{get:function(){return this._sessionId},set:function(e){this._sessionId!==e&&(this._sessionId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"optOut",{get:function(){return this._optOut},set:function(e){this._optOut!==e&&(this._optOut=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"lastEventTime",{get:function(){return this._lastEventTime},set:function(e){this._lastEventTime!==e&&(this._lastEventTime=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"lastEventId",{get:function(){return this._lastEventId},set:function(e){this._lastEventId!==e&&(this._lastEventId=e,this.updateStorage())},enumerable:!1,configurable:!0}),t.prototype.updateStorage=function(){var e,t={deviceId:this._deviceId,userId:this._userId,sessionId:this._sessionId,optOut:this._optOut,lastEventTime:this._lastEventTime,lastEventId:this._lastEventId};null===(e=this.cookieStorage)||void 0===e||e.set(we(this.apiKey),t)},t}(oe),Ve=function(e,t){return f(void 0,void 0,void 0,(function(){var i,n,r,o,s,a,u,c,l,p,f,h,g,b,y;return v(this,(function(v){switch(v.label){case 0:return i=Le(),n=null!==(b=null==t?void 0:t.deviceId)&&void 0!==b?b:be(),r=null==t?void 0:t.lastEventId,o=null==t?void 0:t.lastEventTime,s=null==t?void 0:t.optOut,a=null==t?void 0:t.sessionId,u=null==t?void 0:t.userId,c=null==t?void 0:t.cookieStorage,l=null==t?void 0:t.domain,p=Me.bind,f=[void 0,e],h=[d({},t)],g={cookieStorage:c,deviceId:n,domain:l,lastEventId:r,lastEventTime:o,optOut:s,sessionId:a},[4,Be(t)];case 1:return[2,new(p.apply(Me,f.concat([d.apply(void 0,h.concat([(g.storageProvider=v.sent(),g.trackingOptions=d(d({},i.trackingOptions),null==t?void 0:t.trackingOptions),g.transportProvider=null!==(y=null==t?void 0:t.transportProvider)&&void 0!==y?y:$e(null==t?void 0:t.transport),g.userId=u,g)]))])))]}}))}))},ze=function(e,t){return void 0===t&&(t=Le()),f(void 0,void 0,void 0,(function(){var i,n,r;return v(this,(function(o){switch(o.label){case 0:return i=d(d({},t),e),n=null==e?void 0:e.cookieStorage,(r=!n)?[3,2]:[4,n.isEnabled()];case 1:r=!o.sent(),o.label=2;case 2:return r?[2,Fe(i)]:[2,n]}}))}))},Fe=function(e){return f(void 0,void 0,void 0,(function(){var t,i;return v(this,(function(n){switch(n.label){case 0:return t=new ke({domain:e.domain,expirationDays:e.cookieExpiration,sameSite:e.cookieSameSite,secure:e.cookieSecure}),(i=e.disableCookies)?[3,2]:[4,t.isEnabled()];case 1:i=!n.sent(),n.label=2;case 2:return i?[4,(t=new Ae).isEnabled()]:[3,4];case 3:n.sent()||(t=new ye),n.label=4;case 4:return[2,t]}}))}))},Be=function(e){return f(void 0,void 0,void 0,(function(){var t,i,n,r,o,s,a,u,c;return v(this,(function(l){switch(l.label){case 0:if(t=e&&Object.prototype.hasOwnProperty.call(e,"storageProvider"),i=e&&e.loggerProvider,t&&!e.storageProvider)return[3,9];l.label=1;case 1:l.trys.push([1,7,8,9]),n=h([null==e?void 0:e.storageProvider,new Ae({loggerProvider:i})]),r=n.next(),l.label=2;case 2:return r.done?[3,6]:(o=r.value,(s=o)?[4,o.isEnabled()]:[3,4]);case 3:s=l.sent(),l.label=4;case 4:if(s)return[2,o];l.label=5;case 5:return r=n.next(),[3,2];case 6:return[3,9];case 7:return a=l.sent(),u={error:a},[3,9];case 8:try{r&&!r.done&&(c=n.return)&&c.call(n)}finally{if(u)throw u.error}return[7];case 9:return[2,void 0]}}))}))},$e=function(e){return e===S.XHR?new Ce:e===S.SendBeacon?new je:Le().transportProvider},Qe="[Amplitude]",Ke="".concat(Qe," Page Viewed"),He="".concat(Qe," Form Started"),We="".concat(Qe," Form Submitted"),Ze="".concat(Qe," File Downloaded"),Ge="session_start",Je="session_end",Ye="".concat(Qe," File Extension"),Xe="".concat(Qe," File Name"),et="".concat(Qe," Link ID"),tt="".concat(Qe," Link Text"),it="".concat(Qe," Link URL"),nt="".concat(Qe," Form ID"),rt="".concat(Qe," Form Name"),ot="".concat(Qe," Form Destination"),st=function(e,t){return f(void 0,void 0,void 0,(function(){var i,n,r,o,s,a,u,c,l,d,p;return v(this,(function(f){switch(f.label){case 0:return[4,ze(t)];case 1:return i=f.sent(),n=function(e){return"".concat(B.toLowerCase(),"_").concat(e.substring(0,6))}(e),[4,i.getRaw(n)];case 2:return(r=f.sent())?(null!==(p=t.cookieUpgrade)&&void 0!==p?p:Le().cookieUpgrade)?[4,i.remove(n)]:[3,4]:[2,{optOut:!1}];case 3:f.sent(),f.label=4;case 4:return o=g(r.split("."),6),s=o[0],a=o[1],u=o[2],c=o[3],l=o[4],d=o[5],[2,{deviceId:s,userId:ut(a),sessionId:at(c),lastEventId:at(d),lastEventTime:at(l),optOut:Boolean(u)}]}}))}))},at=function(e){var t=parseInt(e,32);if(!isNaN(t))return t},ut=function(e){if(atob&&escape&&e)try{return decodeURIComponent(escape(atob(e)))}catch(e){return}},ct="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},lt={exports:{}};ce=lt,le=lt.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",O="Huawei",T="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",q="Sharp",U="Sony",D="Xiaomi",A="Zebra",C="Facebook",j=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},F=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o350?V(e,350):e,this},this.setUA(f),this};Q.VERSION="0.7.33",Q.BROWSER=j([a,l,"major"]),Q.CPU=j([d]),Q.DEVICE=j([s,c,u,p,f,h,v,g,b]),Q.ENGINE=Q.OS=j([a,l]),ce.exports&&(le=ce.exports=Q),le.UAParser=Q;var K=typeof e!==n&&(e.jQuery||e.Zepto);if(K&&!K.ua){var H=new Q;K.ua=H.getResult(),K.ua.get=function(){return H.getUA()},K.ua.set=function(e){H.setUA(e);var t=H.getResult();for(var i in t)K.ua[i]=t[i]}}}("object"==typeof window?window:ct);var dt=lt.exports,pt=function(){function e(){this.name="context",this.type=I.BEFORE,this.library="amplitude-ts/".concat("1.13.0"),"undefined"!=typeof navigator&&(this.userAgent=navigator.userAgent),this.uaResult=new dt(this.userAgent).getResult()}return e.prototype.setup=function(e){return this.config=e,Promise.resolve(void 0)},e.prototype.execute=function(e){var t,i;return f(this,void 0,void 0,(function(){var n,r,o,s,a,u,c;return v(this,(function(l){return n=(new Date).getTime(),r=this.uaResult.browser.name,o=this.uaResult.browser.version,s=this.uaResult.device.model||this.uaResult.os.name,a=this.uaResult.device.vendor,u=null!==(t=this.config.lastEventId)&&void 0!==t?t:-1,c=null!==(i=e.event_id)&&void 0!==i?i:u+1,this.config.lastEventId=c,e.time||(this.config.lastEventTime=n),[2,d(d(d(d(d(d(d(d(d(d(d(d({user_id:this.config.userId,device_id:this.config.deviceId,session_id:this.config.sessionId,time:n},this.config.appVersion&&{app_version:this.config.appVersion}),this.config.trackingOptions.platform&&{platform:"Web"}),this.config.trackingOptions.osName&&{os_name:r}),this.config.trackingOptions.osVersion&&{os_version:o}),this.config.trackingOptions.deviceManufacturer&&{device_manufacturer:a}),this.config.trackingOptions.deviceModel&&{device_model:s}),this.config.trackingOptions.language&&{language:_e()}),this.config.trackingOptions.ipAddress&&{ip:"$remote"}),{insert_id:be(),partner_id:this.config.partnerId,plan:this.config.plan}),this.config.ingestionMetadata&&{ingestion_metadata:{source_name:this.config.ingestionMetadata.sourceName,source_version:this.config.ingestionMetadata.sourceVersion}}),e),{event_id:c,library:this.library,user_agent:this.userAgent})]}))}))},e}(),ft={page_domain:"".concat(Qe," Page Domain"),page_location:"".concat(Qe," Page Location"),page_path:"".concat(Qe," Page Path"),page_title:"".concat(Qe," Page Title"),page_url:"".concat(Qe," Page URL")},vt=function(){var e,t=[];return{name:"@amplitude/plugin-file-download-tracking-browser",type:I.ENRICHMENT,setup:function(i,n){return f(void 0,void 0,void 0,(function(){var r,o;return v(this,(function(s){return n?("undefined"==typeof document||(r=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=o.exec(i.href),s=null==r?void 0:r[1];s&&function(e,i,n){e.addEventListener(i,n),t.push({element:e,type:i,handler:n})}(e,"click",(function(){var t;s&&n.track(Ze,((t={})[Ye]=s,t[Xe]=i.pathname,t[et]=e.id,t[tt]=e.text,t[it]=e.href,t))}))},o=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(r),"undefined"!=typeof MutationObserver&&(e=new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&r(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(r)}))}))}))).observe(document.body,{subtree:!0,childList:!0})),[2]):(i.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return f(void 0,void 0,void 0,(function(){return v(this,(function(t){return[2,e]}))}))},teardown:function(){return f(void 0,void 0,void 0,(function(){return v(this,(function(i){return null==e||e.disconnect(),t.forEach((function(e){var t=e.element,i=e.type,n=e.handler;null==t||t.removeEventListener(i,n)})),t=[],[2]}))}))}}},ht=function(){var e,t=[],i=function(e,i,n){e.addEventListener(i,n),t.push({element:e,type:i,handler:n})};return{name:"@amplitude/plugin-form-interaction-tracking-browser",type:I.ENRICHMENT,setup:function(t,n){return f(void 0,void 0,void 0,(function(){var r;return v(this,(function(o){return n?("undefined"==typeof document||(r=function(e){var t=!1;i(e,"change",(function(){var i;t||n.track(He,((i={})[nt]=e.id,i[rt]=e.name,i[ot]=e.action,i)),t=!0})),i(e,"submit",(function(){var i,r;t||n.track(He,((i={})[nt]=e.id,i[rt]=e.name,i[ot]=e.action,i)),n.track(We,((r={})[nt]=e.id,r[rt]=e.name,r[ot]=e.action,r)),t=!1}))},Array.from(document.getElementsByTagName("form")).forEach(r),"undefined"!=typeof MutationObserver&&(e=new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&r(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(r)}))}))}))).observe(document.body,{subtree:!0,childList:!0})),[2]):(t.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return f(void 0,void 0,void 0,(function(){return v(this,(function(t){return[2,e]}))}))},teardown:function(){return f(void 0,void 0,void 0,(function(){return v(this,(function(i){return null==e||e.disconnect(),t.forEach((function(e){var t=e.element,i=e.type,n=e.handler;null==t||t.removeEventListener(i,n)})),t=[],[2]}))}))}}},gt=function(e,t){bt(e,t)},bt=function(e,t){for(var i=0;i=0;--r)i.push(t.slice(r).join("."));r=0,a.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(null!==(_=null!==(w=t.sessionId)&&void 0!==w?w:this.config.sessionId)&&void 0!==_?_:Date.now()),$=!0),(Q=u(t.instanceName)).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new de).promise];case 11:return Z.sent(),[4,this.add(new pt).promise];case 12:return Z.sent(),[4,this.add(new Ie).promise];case 13:return Z.sent(),("boolean"==typeof(G=this.config.defaultTracking)?G:null==G?void 0:G.fileDownloads)?[4,this.add(vt()).promise]:[3,15];case 14:Z.sent(),Z.label=15;case 15:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add(ht()).promise]:[3,17];case 16:Z.sent(),Z.label=17;case 17:return(null===(k=this.config.attribution)||void 0===k?void 0:k.disabled)?[3,19]:(K=function(){for(var e,t,i=this,n=[],r=0;rthis.config.sessionTimeout&&this.setSessionId(i),[2,e.prototype.process.call(this,t)]}))}))},t}(ee),wt=function(){var e=new mt;return{init:ge(e.init.bind(e),"init",fe(e),he(e,["config"])),add:ge(e.add.bind(e),"add",fe(e),he(e,["config.apiKey","timeline.plugins"])),remove:ge(e.remove.bind(e),"remove",fe(e),he(e,["config.apiKey","timeline.plugins"])),track:ge(e.track.bind(e),"track",fe(e),he(e,["config.apiKey","timeline.queue.length"])),logEvent:ge(e.logEvent.bind(e),"logEvent",fe(e),he(e,["config.apiKey","timeline.queue.length"])),identify:ge(e.identify.bind(e),"identify",fe(e),he(e,["config.apiKey","timeline.queue.length"])),groupIdentify:ge(e.groupIdentify.bind(e),"groupIdentify",fe(e),he(e,["config.apiKey","timeline.queue.length"])),setGroup:ge(e.setGroup.bind(e),"setGroup",fe(e),he(e,["config.apiKey","timeline.queue.length"])),revenue:ge(e.revenue.bind(e),"revenue",fe(e),he(e,["config.apiKey","timeline.queue.length"])),flush:ge(e.flush.bind(e),"flush",fe(e),he(e,["config.apiKey","timeline.queue.length"])),getUserId:ge(e.getUserId.bind(e),"getUserId",fe(e),he(e,["config","config.userId"])),setUserId:ge(e.setUserId.bind(e),"setUserId",fe(e),he(e,["config","config.userId"])),getDeviceId:ge(e.getDeviceId.bind(e),"getDeviceId",fe(e),he(e,["config","config.deviceId"])),setDeviceId:ge(e.setDeviceId.bind(e),"setDeviceId",fe(e),he(e,["config","config.deviceId"])),reset:ge(e.reset.bind(e),"reset",fe(e),he(e,["config","config.userId","config.deviceId"])),getSessionId:ge(e.getSessionId.bind(e),"getSessionId",fe(e),he(e,["config"])),setSessionId:ge(e.setSessionId.bind(e),"setSessionId",fe(e),he(e,["config"])),extendSession:ge(e.extendSession.bind(e),"extendSession",fe(e),he(e,["config"])),setOptOut:ge(e.setOptOut.bind(e),"setOptOut",fe(e),he(e,["config"])),setTransport:ge(e.setTransport.bind(e),"setTransport",fe(e),he(e,["config"]))}},_t=wt(),It=_t.add,kt=_t.extendSession,Et=_t.flush,St=_t.getDeviceId,Ot=_t.getSessionId,Tt=_t.getUserId,xt=_t.groupIdentify,Pt=_t.identify,Rt=_t.init,Nt=_t.logEvent,qt=_t.remove,Ut=_t.reset,Dt=_t.revenue,At=_t.setDeviceId,Ct=_t.setGroup,jt=_t.setOptOut,Lt=_t.setSessionId,Mt=_t.setTransport,Vt=_t.setUserId,zt=_t.track,Ft=Object.freeze({__proto__:null,add:It,extendSession:kt,flush:Et,getDeviceId:St,getSessionId:Ot,getUserId:Tt,groupIdentify:xt,identify:Pt,init:Rt,logEvent:Nt,remove:qt,reset:Ut,revenue:Dt,setDeviceId:At,setGroup:Ct,setOptOut:jt,setSessionId:Lt,setTransport:Mt,setUserId:Vt,track:zt,Types:F,createInstance:wt,runQueuedFunctions:gt,Revenue:te,Identify:W});!function(){var e=O();if(e){var t=function(e){var t=wt(),i=O();return i&&i.amplitude&&i.amplitude._iq&&e&&(i.amplitude._iq[e]=t),t};if(e.amplitude=Object.assign(e.amplitude||{},Ft,{createInstance:t}),e.amplitude.invoked){var i=e.amplitude._q;e.amplitude._q=[],gt(Ft,i);for(var n=Object.keys(e.amplitude._iq)||[],r=0;r0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),n=function(){return n=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function g(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function b(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!H(t,i))return!1}return!0},H=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=h(t),s=o.next();!s.done;s=o.next()){var a=s.value;if(Array.isArray(a))return!1;if("object"==typeof a)r=r&&K(a);else if(!["number","string"].includes(typeof a))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{s&&!s.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return K(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},W=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return d({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(y.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(y.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(y.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(y.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(y.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(y.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(y.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(y.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(y.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[y.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[y.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===y.ADD?"number"==typeof i:e===y.UNSET||e===y.REMOVE||H(t,i)))},e}(),Z=function(e,t){return d(d({},t),{event_type:w.IDENTIFY,user_properties:e.getUserProperties()})},G=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=E.Unknown),{event:e,code:t,message:i}},J=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return f(this,void 0,void 0,(function(){return v(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){var t;return f(this,void 0,void 0,(function(){var i,n;return v(this,(function(r){switch(r.label){case 0:return i=this.plugins.findIndex((function(t){return t.name===e})),n=this.plugins[i],this.plugins.splice(i,1),[4,null===(t=n.teardown)||void 0===t?void 0:t.call(n)];case 1:return r.sent(),[2]}}))}))},e.prototype.reset=function(e){this.applying=!1,this.plugins.map((function(e){var t;return null===(t=e.teardown)||void 0===t?void 0:t.call(e)})),this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return f(this,void 0,void 0,(function(){var t,i,n,r,o,s,a,u,c,l,p,f,b,y,m,w,_,k,E;return v(this,(function(v){switch(v.label){case 0:if(!e)return[2];t=g(e,1),i=t[0],n=g(e,2),r=n[1],o=this.plugins.filter((function(e){return e.type===I.BEFORE})),v.label=1;case 1:v.trys.push([1,6,7,8]),s=h(o),a=s.next(),v.label=2;case 2:return a.done?[3,5]:[4,a.value.execute(d({},i))];case 3:if(null===(f=v.sent()))return r({event:i,code:0,message:""}),[2];i=f,v.label=4;case 4:return a=s.next(),[3,2];case 5:return[3,8];case 6:return u=v.sent(),w={error:u},[3,8];case 7:try{a&&!a.done&&(_=s.return)&&_.call(s)}finally{if(w)throw w.error}return[7];case 8:c=this.plugins.filter((function(e){return e.type===I.ENRICHMENT})),v.label=9;case 9:v.trys.push([9,14,15,16]),l=h(c),p=l.next(),v.label=10;case 10:return p.done?[3,13]:[4,p.value.execute(d({},i))];case 11:if(null===(f=v.sent()))return r({event:i,code:0,message:""}),[2];i=f,v.label=12;case 12:return p=l.next(),[3,10];case 13:return[3,16];case 14:return b=v.sent(),k={error:b},[3,16];case 15:try{p&&!p.done&&(E=l.return)&&E.call(l)}finally{if(k)throw k.error}return[7];case 16:return y=this.plugins.filter((function(e){return e.type===I.DESTINATION})),m=y.map((function(e){var t=d({},i);return e.execute(t).catch((function(e){return G(t,0,String(e))}))})),Promise.all(m).then((function(e){var t=g(e,1)[0];r(t)})),[2]}}))}))},e.prototype.flush=function(){return f(this,void 0,void 0,(function(){var e,t,i,n=this;return v(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===I.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),Y="Event rejected due to exceeded retry count",X=function(e){return{promise:e||Promise.resolve()}},ee=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new J(this),this.name=e}return e.prototype._init=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return f(this,void 0,void 0,(function(){var t,i,n,r,o,s;return v(this,(function(a){switch(a.label){case 0:t=this[e],this[e]=[],a.label=1;case 1:a.trys.push([1,6,7,8]),i=h(t),n=i.next(),a.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:a.sent(),a.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=a.sent(),o={error:r},[3,8];case 7:try{n&&!n.done&&(s=i.return)&&s.call(i)}finally{if(o)throw o.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,i){var n=function(e,t,i){return d(d(d({},"string"==typeof e?{event_type:e}:e),i),t&&{event_properties:t})}(e,t,i);return X(this.dispatch(n))},e.prototype.identify=function(e,t){var i=Z(e,t);return X(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,i,n){var r=function(e,t,i,n){var r;return d(d({},n),{event_type:w.GROUP_IDENTIFY,group_properties:i.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,i,n);return X(this.dispatch(r))},e.prototype.setGroup=function(e,t,i){var n=function(e,t,i){var n,r=new W;return r.set(e,t),d(d({},i),{event_type:w.IDENTIFY,user_properties:r.getUserProperties(),groups:(n={},n[e]=t,n)})}(e,t,i);return X(this.dispatch(n))},e.prototype.revenue=function(e,t){var i=function(e,t){return d(d({},t),{event_type:w.REVENUE,event_properties:e.getEventProperties()})}(e,t);return X(this.dispatch(i))},e.prototype.add=function(e){return this.config?X(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),X())},e.prototype.remove=function(e){return this.config?X(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),X())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(G(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return f(this,void 0,void 0,(function(){var t=this;return v(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return f(this,void 0,void 0,(function(){var t,i,n;return v(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,G(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=G(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return X(this.timeline.flush())},e}(),te=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return K(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?d({},this.properties):{};return e[m.REVENUE_PRODUCT_ID]=this.productId,e[m.REVENUE_QUANTITY]=this.quantity,e[m.REVENUE_PRICE]=this.price,e[m.REVENUE_TYPE]=this.revenueType,e[m.REVENUE]=this.revenue,e},e}(),ie="Amplitude Logger ",ne=function(){function e(){this.logLevel=_.None}return e.prototype.disable=function(){this.logLevel=_.None},e.prototype.enable=function(e){void 0===e&&(e=_.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),f(this,void 0,void 0,(function(){var t,i,n,r=this;return v(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),f(this,void 0,void 0,(function(){var i,n,r,o,s;return v(this,(function(a){switch(a.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,p(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},a.label=1;case 1:return a.trys.push([1,3,,4]),n=ae(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(n,i)];case 2:return null===(r=a.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(r,e),[3,4]):("body"in r?this.fulfillRequest(e,r.statusCode,"".concat(r.status,": ").concat(ue(r))):this.fulfillRequest(e,r.statusCode,r.status),[2]);case 3:return o=a.sent(),s=(u=o)instanceof Error?u.message:String(u),this.config.loggerProvider.error(s),this.fulfillRequest(e,0,s),[3,4];case 4:return[2]}var u}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case E.Success:this.handleSuccessResponse(e,t);break;case E.Invalid:this.handleInvalidResponse(e,t);break;case E.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case E.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=b(b(b(b([],g(Object.values(e.body.eventsWithInvalidFields)),!1),g(Object.values(e.body.eventsWithMissingFields)),!1),g(Object.values(e.body.eventsWithInvalidIdLengths)),!1),g(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(ue(e)),this.addToQueue.apply(this,b([],g(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(ue(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,b([],g(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),a=new Set(r),u=new Set(o),c=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&a.has(t.event.device_id)))return u.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));c.length>0&&this.config.loggerProvider.warn(ue(e)),this.addToQueue.apply(this,b([],g(c),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,b([],g(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(G(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),pe=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},fe=function(e){return function(){var t=d({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},ve=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=h(t.split(".")),o=r.next();!o.done;o=r.next()){var s=o.value;if(!(s in e))return;e=e[s]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},he=function(e,t){return function(){var i,n,r={};try{for(var o=h(t),s=o.next();!s.done;s=o.next()){var a=s.value;r[a]=ve(e,a)}}catch(e){i={error:e}}finally{try{s&&!s.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ge=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,be)},ye=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return f(this,void 0,void 0,(function(){return v(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return f(this,void 0,void 0,(function(){var t;return v(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return f(this,void 0,void 0,(function(){return v(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return f(this,void 0,void 0,(function(){return v(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),me=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,h,g,b,y,m,w,_,I;if("object"!=typeof e)return null;var k=e.code||0,S=this.buildStatus(k);switch(S){case E.Success:return{status:S,statusCode:k,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case E.Invalid:return{status:S,statusCode:k,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case E.PayloadTooLarge:return{status:S,statusCode:k,body:{error:null!==(h=e.error)&&void 0!==h?h:""}};case E.RateLimit:return{status:S,statusCode:k,body:{error:null!==(g=e.error)&&void 0!==g?g:"",epsThreshold:null!==(b=e.eps_threshold)&&void 0!==b?b:0,throttledDevices:null!==(y=e.throttled_devices)&&void 0!==y?y:{},throttledUsers:null!==(m=e.throttled_users)&&void 0!==m?m:{},exceededDailyQuotaDevices:null!==(w=e.exceeded_daily_quota_devices)&&void 0!==w?w:{},exceededDailyQuotaUsers:null!==(_=e.exceeded_daily_quota_users)&&void 0!==_?_:{},throttledEvents:null!==(I=e.throttled_events)&&void 0!==I?I:[]}};case E.Timeout:default:return{status:S,statusCode:k}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?E.Success:429===e?E.RateLimit:413===e?E.PayloadTooLarge:408===e?E.Timeout:e>=400&&e<500?E.Invalid:e>=500?E.Failed:E.Unknown},e}(),we=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[B,t,e.substring(0,i)].filter(Boolean).join("_")},_e=function(){var e,t,i,n;if("undefined"==typeof navigator)return"";var r=navigator.userLanguage;return null!==(n=null!==(i=null!==(t=null===(e=navigator.languages)||void 0===e?void 0:e[0])&&void 0!==t?t:navigator.language)&&void 0!==i?i:r)&&void 0!==n?n:""},Ie=function(){function e(){this.name="identity",this.type=I.BEFORE,this.identityStore=u().identityStore}return e.prototype.execute=function(e){return f(this,void 0,void 0,(function(){var t;return v(this,(function(i){return(t=e.user_properties)&&this.identityStore.editIdentity().updateUserProperties(t).commit(),[2,e]}))}))},e.prototype.setup=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){return e.instanceName&&(this.identityStore=u(e.instanceName).identityStore),[2]}))}))},e}(),ke=function(){function e(e){this.options=d({},e)}return e.prototype.isEnabled=function(){return f(this,void 0,void 0,(function(){var t,i;return v(this,(function(n){switch(n.label){case 0:if(!O())return[2,!1];e.testValue=String(Date.now()),t=new e(this.options),i="AMP_TEST",n.label=1;case 1:return n.trys.push([1,4,5,7]),[4,t.set(i,e.testValue)];case 2:return n.sent(),[4,t.get(i)];case 3:return[2,n.sent()===e.testValue];case 4:return n.sent(),[2,!1];case 5:return[4,t.remove(i)];case 6:return n.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return f(this,void 0,void 0,(function(){var t;return v(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return f(this,void 0,void 0,(function(){var i,n,r;return v(this,(function(o){return i=O(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return f(this,void 0,void 0,(function(){var n,r,o,s,a,u;return v(this,(function(c){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,o=void 0,(r=null!==t?n:-1)&&((s=new Date).setTime(s.getTime()+24*r*60*60*1e3),o=s),a="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),o&&(a+="; expires=".concat(o.toUTCString())),a+="; path=/",this.options.domain&&(a+="; domain=".concat(this.options.domain)),this.options.secure&&(a+="; Secure"),this.options.sameSite&&(a+="; SameSite=".concat(this.options.sameSite)),(u=O())&&(u.document.cookie=a)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return f(this,void 0,void 0,(function(){return v(this,(function(e){return[2]}))}))},e}(),Ee=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return l(t,e),t.prototype.send=function(e,t){return f(this,void 0,void 0,(function(){var i,n;return v(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},t}(me),Se=function(e){var t={};for(var i in e){var n=e[i];n&&(t[i]=n)}return t},Oe=function(){for(var e,t=[],i=0;iDe;try{o=r?JSON.stringify(t.slice(0,De)):JSON.stringify(t),null===(i=O())||void 0===i||i.localStorage.setItem(e,o)}catch(e){}return r&&(s=t.length-De,null===(n=this.loggerProvider)||void 0===n||n.error("Failed to save ".concat(s," events because the queue length exceeded ").concat(De,"."))),[2]}))}))},e.prototype.remove=function(e){var t;return f(this,void 0,void 0,(function(){return v(this,(function(i){try{null===(t=O())||void 0===t||t.localStorage.removeItem(e)}catch(e){}return[2]}))}))},e.prototype.reset=function(){var e;return f(this,void 0,void 0,(function(){return v(this,(function(t){try{null===(e=O())||void 0===e||e.localStorage.clear()}catch(e){}return[2]}))}))},e}(),Ce=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.state={done:4},t}return l(t,e),t.prototype.send=function(e,t){return f(this,void 0,void 0,(function(){var i=this;return v(this,(function(n){return[2,new Promise((function(n,r){"undefined"==typeof XMLHttpRequest&&r(new Error("XHRTransport is not supported."));var o=new XMLHttpRequest;o.open("POST",e,!0),o.onreadystatechange=function(){if(o.readyState===i.state.done)try{var e=o.responseText,t=JSON.parse(e),s=i.buildResponse(t);n(s)}catch(e){r(e)}},o.setRequestHeader("Content-Type","application/json"),o.setRequestHeader("Accept","*/*"),o.send(JSON.stringify(t))}))]}))}))},t}(me),je=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return l(t,e),t.prototype.send=function(e,t){return f(this,void 0,void 0,(function(){var i=this;return v(this,(function(n){return[2,new Promise((function(n,r){var o=O();if(!(null==o?void 0:o.navigator.sendBeacon))throw new Error("SendBeaconTransport is not supported");try{var s=JSON.stringify(t);return n(o.navigator.sendBeacon(e,JSON.stringify(t))?i.buildResponse({code:200,events_ingested:t.events.length,payload_size_bytes:s.length,server_upload_time:Date.now()}):i.buildResponse({code:500}))}catch(e){r(e)}}))]}))}))},t}(me),Le=function(){return{cookieExpiration:365,cookieSameSite:"Lax",cookieSecure:!1,cookieStorage:new ye,cookieUpgrade:!0,disableCookies:!1,domain:"",sessionTimeout:18e5,trackingOptions:{deviceManufacturer:!0,deviceModel:!0,ipAddress:!0,language:!0,osName:!0,osVersion:!0,platform:!0},transportProvider:new Ee}},Me=function(e){function t(t,i){var n,r,o,s,a,u,c,l,p,f=this,v=Le();return(f=e.call(this,d(d({flushIntervalMillis:1e3,flushMaxRetries:5,flushQueueSize:30,transportProvider:v.transportProvider},i),{apiKey:t}))||this)._optOut=!1,f.cookieStorage=null!==(n=null==i?void 0:i.cookieStorage)&&void 0!==n?n:v.cookieStorage,f.deviceId=null==i?void 0:i.deviceId,f.lastEventId=null==i?void 0:i.lastEventId,f.lastEventTime=null==i?void 0:i.lastEventTime,f.optOut=Boolean(null==i?void 0:i.optOut),f.sessionId=null==i?void 0:i.sessionId,f.userId=null==i?void 0:i.userId,f.appVersion=null==i?void 0:i.appVersion,f.attribution=null==i?void 0:i.attribution,f.cookieExpiration=null!==(r=null==i?void 0:i.cookieExpiration)&&void 0!==r?r:v.cookieExpiration,f.cookieSameSite=null!==(o=null==i?void 0:i.cookieSameSite)&&void 0!==o?o:v.cookieSameSite,f.cookieSecure=null!==(s=null==i?void 0:i.cookieSecure)&&void 0!==s?s:v.cookieSecure,f.cookieUpgrade=null!==(a=null==i?void 0:i.cookieUpgrade)&&void 0!==a?a:v.cookieUpgrade,f.defaultTracking=null==i?void 0:i.defaultTracking,f.disableCookies=null!==(u=null==i?void 0:i.disableCookies)&&void 0!==u?u:v.disableCookies,f.defaultTracking=null==i?void 0:i.defaultTracking,f.domain=null!==(c=null==i?void 0:i.domain)&&void 0!==c?c:v.domain,f.partnerId=null==i?void 0:i.partnerId,f.sessionTimeout=null!==(l=null==i?void 0:i.sessionTimeout)&&void 0!==l?l:v.sessionTimeout,f.trackingOptions=null!==(p=null==i?void 0:i.trackingOptions)&&void 0!==p?p:v.trackingOptions,f}return l(t,e),Object.defineProperty(t.prototype,"deviceId",{get:function(){return this._deviceId},set:function(e){this._deviceId!==e&&(this._deviceId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"userId",{get:function(){return this._userId},set:function(e){this._userId!==e&&(this._userId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"sessionId",{get:function(){return this._sessionId},set:function(e){this._sessionId!==e&&(this._sessionId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"optOut",{get:function(){return this._optOut},set:function(e){this._optOut!==e&&(this._optOut=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"lastEventTime",{get:function(){return this._lastEventTime},set:function(e){this._lastEventTime!==e&&(this._lastEventTime=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"lastEventId",{get:function(){return this._lastEventId},set:function(e){this._lastEventId!==e&&(this._lastEventId=e,this.updateStorage())},enumerable:!1,configurable:!0}),t.prototype.updateStorage=function(){var e,t={deviceId:this._deviceId,userId:this._userId,sessionId:this._sessionId,optOut:this._optOut,lastEventTime:this._lastEventTime,lastEventId:this._lastEventId};null===(e=this.cookieStorage)||void 0===e||e.set(we(this.apiKey),t)},t}(oe),Ve=function(e,t){return f(void 0,void 0,void 0,(function(){var i,n,r,o,s,a,u,c,l,p,f,h,g,b,y;return v(this,(function(v){switch(v.label){case 0:return i=Le(),n=null!==(b=null==t?void 0:t.deviceId)&&void 0!==b?b:be(),r=null==t?void 0:t.lastEventId,o=null==t?void 0:t.lastEventTime,s=null==t?void 0:t.optOut,a=null==t?void 0:t.sessionId,u=null==t?void 0:t.userId,c=null==t?void 0:t.cookieStorage,l=null==t?void 0:t.domain,p=Me.bind,f=[void 0,e],h=[d({},t)],g={cookieStorage:c,deviceId:n,domain:l,lastEventId:r,lastEventTime:o,optOut:s,sessionId:a},[4,Be(t)];case 1:return[2,new(p.apply(Me,f.concat([d.apply(void 0,h.concat([(g.storageProvider=v.sent(),g.trackingOptions=d(d({},i.trackingOptions),null==t?void 0:t.trackingOptions),g.transportProvider=null!==(y=null==t?void 0:t.transportProvider)&&void 0!==y?y:$e(null==t?void 0:t.transport),g.userId=u,g)]))])))]}}))}))},ze=function(e,t){return void 0===t&&(t=Le()),f(void 0,void 0,void 0,(function(){var i,n,r;return v(this,(function(o){switch(o.label){case 0:return i=d(d({},t),e),n=null==e?void 0:e.cookieStorage,(r=!n)?[3,2]:[4,n.isEnabled()];case 1:r=!o.sent(),o.label=2;case 2:return r?[2,Fe(i)]:[2,n]}}))}))},Fe=function(e){return f(void 0,void 0,void 0,(function(){var t,i;return v(this,(function(n){switch(n.label){case 0:return t=new ke({domain:e.domain,expirationDays:e.cookieExpiration,sameSite:e.cookieSameSite,secure:e.cookieSecure}),(i=e.disableCookies)?[3,2]:[4,t.isEnabled()];case 1:i=!n.sent(),n.label=2;case 2:return i?[4,(t=new Ae).isEnabled()]:[3,4];case 3:n.sent()||(t=new ye),n.label=4;case 4:return[2,t]}}))}))},Be=function(e){return f(void 0,void 0,void 0,(function(){var t,i,n,r,o,s,a,u,c;return v(this,(function(l){switch(l.label){case 0:if(t=e&&Object.prototype.hasOwnProperty.call(e,"storageProvider"),i=e&&e.loggerProvider,t&&!e.storageProvider)return[3,9];l.label=1;case 1:l.trys.push([1,7,8,9]),n=h([null==e?void 0:e.storageProvider,new Ae({loggerProvider:i})]),r=n.next(),l.label=2;case 2:return r.done?[3,6]:(o=r.value,(s=o)?[4,o.isEnabled()]:[3,4]);case 3:s=l.sent(),l.label=4;case 4:if(s)return[2,o];l.label=5;case 5:return r=n.next(),[3,2];case 6:return[3,9];case 7:return a=l.sent(),u={error:a},[3,9];case 8:try{r&&!r.done&&(c=n.return)&&c.call(n)}finally{if(u)throw u.error}return[7];case 9:return[2,void 0]}}))}))},$e=function(e){return e===S.XHR?new Ce:e===S.SendBeacon?new je:Le().transportProvider},Qe="[Amplitude]",Ke="".concat(Qe," Page Viewed"),He="".concat(Qe," Form Started"),We="".concat(Qe," Form Submitted"),Ze="".concat(Qe," File Downloaded"),Ge="session_start",Je="session_end",Ye="".concat(Qe," File Extension"),Xe="".concat(Qe," File Name"),et="".concat(Qe," Link ID"),tt="".concat(Qe," Link Text"),it="".concat(Qe," Link URL"),nt="".concat(Qe," Form ID"),rt="".concat(Qe," Form Name"),ot="".concat(Qe," Form Destination"),st=function(e,t){return f(void 0,void 0,void 0,(function(){var i,n,r,o,s,a,u,c,l,d,p;return v(this,(function(f){switch(f.label){case 0:return[4,ze(t)];case 1:return i=f.sent(),n=function(e){return"".concat(B.toLowerCase(),"_").concat(e.substring(0,6))}(e),[4,i.getRaw(n)];case 2:return(r=f.sent())?(null!==(p=t.cookieUpgrade)&&void 0!==p?p:Le().cookieUpgrade)?[4,i.remove(n)]:[3,4]:[2,{optOut:!1}];case 3:f.sent(),f.label=4;case 4:return o=g(r.split("."),6),s=o[0],a=o[1],u=o[2],c=o[3],l=o[4],d=o[5],[2,{deviceId:s,userId:ut(a),sessionId:at(c),lastEventId:at(d),lastEventTime:at(l),optOut:Boolean(u)}]}}))}))},at=function(e){var t=parseInt(e,32);if(!isNaN(t))return t},ut=function(e){if(atob&&escape&&e)try{return decodeURIComponent(escape(atob(e)))}catch(e){return}},ct="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},lt={exports:{}};ce=lt,le=lt.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",O="Huawei",T="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",q="Sharp",U="Sony",D="Xiaomi",A="Zebra",C="Facebook",j=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},F=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o350?V(e,350):e,this},this.setUA(f),this};Q.VERSION="0.7.33",Q.BROWSER=j([a,l,"major"]),Q.CPU=j([d]),Q.DEVICE=j([s,c,u,p,f,h,v,g,b]),Q.ENGINE=Q.OS=j([a,l]),ce.exports&&(le=ce.exports=Q),le.UAParser=Q;var K=typeof e!==n&&(e.jQuery||e.Zepto);if(K&&!K.ua){var H=new Q;K.ua=H.getResult(),K.ua.get=function(){return H.getUA()},K.ua.set=function(e){H.setUA(e);var t=H.getResult();for(var i in t)K.ua[i]=t[i]}}}("object"==typeof window?window:ct);var dt=lt.exports,pt=function(){function e(){this.name="context",this.type=I.BEFORE,this.library="amplitude-ts/".concat("1.13.1"),"undefined"!=typeof navigator&&(this.userAgent=navigator.userAgent),this.uaResult=new dt(this.userAgent).getResult()}return e.prototype.setup=function(e){return this.config=e,Promise.resolve(void 0)},e.prototype.execute=function(e){var t,i;return f(this,void 0,void 0,(function(){var n,r,o,s,a,u,c;return v(this,(function(l){return n=(new Date).getTime(),r=this.uaResult.browser.name,o=this.uaResult.browser.version,s=this.uaResult.device.model||this.uaResult.os.name,a=this.uaResult.device.vendor,u=null!==(t=this.config.lastEventId)&&void 0!==t?t:-1,c=null!==(i=e.event_id)&&void 0!==i?i:u+1,this.config.lastEventId=c,e.time||(this.config.lastEventTime=n),[2,d(d(d(d(d(d(d(d(d(d(d(d({user_id:this.config.userId,device_id:this.config.deviceId,session_id:this.config.sessionId,time:n},this.config.appVersion&&{app_version:this.config.appVersion}),this.config.trackingOptions.platform&&{platform:"Web"}),this.config.trackingOptions.osName&&{os_name:r}),this.config.trackingOptions.osVersion&&{os_version:o}),this.config.trackingOptions.deviceManufacturer&&{device_manufacturer:a}),this.config.trackingOptions.deviceModel&&{device_model:s}),this.config.trackingOptions.language&&{language:_e()}),this.config.trackingOptions.ipAddress&&{ip:"$remote"}),{insert_id:be(),partner_id:this.config.partnerId,plan:this.config.plan}),this.config.ingestionMetadata&&{ingestion_metadata:{source_name:this.config.ingestionMetadata.sourceName,source_version:this.config.ingestionMetadata.sourceVersion}}),e),{event_id:c,library:this.library,user_agent:this.userAgent})]}))}))},e}(),ft={page_domain:"".concat(Qe," Page Domain"),page_location:"".concat(Qe," Page Location"),page_path:"".concat(Qe," Page Path"),page_title:"".concat(Qe," Page Title"),page_url:"".concat(Qe," Page URL")},vt=function(){var e,t=[];return{name:"@amplitude/plugin-file-download-tracking-browser",type:I.ENRICHMENT,setup:function(i,n){return f(void 0,void 0,void 0,(function(){var r,o;return v(this,(function(s){return n?("undefined"==typeof document||(r=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=o.exec(i.href),s=null==r?void 0:r[1];s&&function(e,i,n){e.addEventListener(i,n),t.push({element:e,type:i,handler:n})}(e,"click",(function(){var t;s&&n.track(Ze,((t={})[Ye]=s,t[Xe]=i.pathname,t[et]=e.id,t[tt]=e.text,t[it]=e.href,t))}))},o=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(r),"undefined"!=typeof MutationObserver&&(e=new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&r(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(r)}))}))}))).observe(document.body,{subtree:!0,childList:!0})),[2]):(i.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return f(void 0,void 0,void 0,(function(){return v(this,(function(t){return[2,e]}))}))},teardown:function(){return f(void 0,void 0,void 0,(function(){return v(this,(function(i){return null==e||e.disconnect(),t.forEach((function(e){var t=e.element,i=e.type,n=e.handler;null==t||t.removeEventListener(i,n)})),t=[],[2]}))}))}}},ht=function(){var e,t=[],i=function(e,i,n){e.addEventListener(i,n),t.push({element:e,type:i,handler:n})};return{name:"@amplitude/plugin-form-interaction-tracking-browser",type:I.ENRICHMENT,setup:function(t,n){return f(void 0,void 0,void 0,(function(){var r;return v(this,(function(o){return n?("undefined"==typeof document||(r=function(e){var t=!1;i(e,"change",(function(){var i;t||n.track(He,((i={})[nt]=e.id,i[rt]=e.name,i[ot]=e.action,i)),t=!0})),i(e,"submit",(function(){var i,r;t||n.track(He,((i={})[nt]=e.id,i[rt]=e.name,i[ot]=e.action,i)),n.track(We,((r={})[nt]=e.id,r[rt]=e.name,r[ot]=e.action,r)),t=!1}))},Array.from(document.getElementsByTagName("form")).forEach(r),"undefined"!=typeof MutationObserver&&(e=new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&r(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(r)}))}))}))).observe(document.body,{subtree:!0,childList:!0})),[2]):(t.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return f(void 0,void 0,void 0,(function(){return v(this,(function(t){return[2,e]}))}))},teardown:function(){return f(void 0,void 0,void 0,(function(){return v(this,(function(i){return null==e||e.disconnect(),t.forEach((function(e){var t=e.element,i=e.type,n=e.handler;null==t||t.removeEventListener(i,n)})),t=[],[2]}))}))}}},gt=function(e,t){bt(e,t)},bt=function(e,t){for(var i=0;i=0;--r)i.push(t.slice(r).join("."));r=0,a.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(null!==(_=null!==(w=t.sessionId)&&void 0!==w?w:this.config.sessionId)&&void 0!==_?_:Date.now()),$=!0),(Q=u(t.instanceName)).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new de).promise];case 11:return Z.sent(),[4,this.add(new pt).promise];case 12:return Z.sent(),[4,this.add(new Ie).promise];case 13:return Z.sent(),("boolean"==typeof(G=this.config.defaultTracking)?G:null==G?void 0:G.fileDownloads)?[4,this.add(vt()).promise]:[3,15];case 14:Z.sent(),Z.label=15;case 15:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add(ht()).promise]:[3,17];case 16:Z.sent(),Z.label=17;case 17:return(null===(k=this.config.attribution)||void 0===k?void 0:k.disabled)?[3,19]:(K=function(){for(var e,t,i=this,n=[],r=0;rthis.config.sessionTimeout&&this.setSessionId(i),[2,e.prototype.process.call(this,t)]}))}))},t}(ee),wt=function(){var e=new mt;return{init:ge(e.init.bind(e),"init",fe(e),he(e,["config"])),add:ge(e.add.bind(e),"add",fe(e),he(e,["config.apiKey","timeline.plugins"])),remove:ge(e.remove.bind(e),"remove",fe(e),he(e,["config.apiKey","timeline.plugins"])),track:ge(e.track.bind(e),"track",fe(e),he(e,["config.apiKey","timeline.queue.length"])),logEvent:ge(e.logEvent.bind(e),"logEvent",fe(e),he(e,["config.apiKey","timeline.queue.length"])),identify:ge(e.identify.bind(e),"identify",fe(e),he(e,["config.apiKey","timeline.queue.length"])),groupIdentify:ge(e.groupIdentify.bind(e),"groupIdentify",fe(e),he(e,["config.apiKey","timeline.queue.length"])),setGroup:ge(e.setGroup.bind(e),"setGroup",fe(e),he(e,["config.apiKey","timeline.queue.length"])),revenue:ge(e.revenue.bind(e),"revenue",fe(e),he(e,["config.apiKey","timeline.queue.length"])),flush:ge(e.flush.bind(e),"flush",fe(e),he(e,["config.apiKey","timeline.queue.length"])),getUserId:ge(e.getUserId.bind(e),"getUserId",fe(e),he(e,["config","config.userId"])),setUserId:ge(e.setUserId.bind(e),"setUserId",fe(e),he(e,["config","config.userId"])),getDeviceId:ge(e.getDeviceId.bind(e),"getDeviceId",fe(e),he(e,["config","config.deviceId"])),setDeviceId:ge(e.setDeviceId.bind(e),"setDeviceId",fe(e),he(e,["config","config.deviceId"])),reset:ge(e.reset.bind(e),"reset",fe(e),he(e,["config","config.userId","config.deviceId"])),getSessionId:ge(e.getSessionId.bind(e),"getSessionId",fe(e),he(e,["config"])),setSessionId:ge(e.setSessionId.bind(e),"setSessionId",fe(e),he(e,["config"])),extendSession:ge(e.extendSession.bind(e),"extendSession",fe(e),he(e,["config"])),setOptOut:ge(e.setOptOut.bind(e),"setOptOut",fe(e),he(e,["config"])),setTransport:ge(e.setTransport.bind(e),"setTransport",fe(e),he(e,["config"]))}},_t=wt(),It=_t.add,kt=_t.extendSession,Et=_t.flush,St=_t.getDeviceId,Ot=_t.getSessionId,Tt=_t.getUserId,xt=_t.groupIdentify,Pt=_t.identify,Rt=_t.init,Nt=_t.logEvent,qt=_t.remove,Ut=_t.reset,Dt=_t.revenue,At=_t.setDeviceId,Ct=_t.setGroup,jt=_t.setOptOut,Lt=_t.setSessionId,Mt=_t.setTransport,Vt=_t.setUserId,zt=_t.track,Ft=Object.freeze({__proto__:null,add:It,extendSession:kt,flush:Et,getDeviceId:St,getSessionId:Ot,getUserId:Tt,groupIdentify:xt,identify:Pt,init:Rt,logEvent:Nt,remove:qt,reset:Ut,revenue:Dt,setDeviceId:At,setGroup:Ct,setOptOut:jt,setSessionId:Lt,setTransport:Mt,setUserId:Vt,track:zt,Types:F,createInstance:wt,runQueuedFunctions:gt,Revenue:te,Identify:W});!function(){var e=O();if(e){var t=function(e){var t=wt(),i=O();return i&&i.amplitude&&i.amplitude._iq&&e&&(i.amplitude._iq[e]=t),t};if(e.amplitude=Object.assign(e.amplitude||{},Ft,{createInstance:t}),e.amplitude.invoked){var i=e.amplitude._q;e.amplitude._q=[],gt(Ft,i);for(var n=Object.keys(e.amplitude._iq)||[],r=0;r ```html diff --git a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js index 8fd59100a..91e30fbbc 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js @@ -30,12 +30,19 @@ }); }; }; + var proxyInstance = function proxyInstance(instance, fn, args) { + instance._q.push({ + name: fn, + args: Array.prototype.slice.call(args, 0) + }); + }; var proxyMain = function proxyMain(instance, fn, isPromise) { var args = arguments; instance[fn] = function () { if (isPromise) return { promise: new Promise(getPromiseResult(instance, fn, Array.prototype.slice.call(arguments))) }; + proxyInstance(instance, fn, Array.prototype.slice.call(arguments)); }; }; var setUpProxy = function setUpProxy(instance) { @@ -49,10 +56,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-KFpf0GoF9T8vImfCj4bKaLU1avoV+yAwIbKzWUj8Od+ejSa2yyirpf0WzAcSioxh'; + as.integrity = 'sha384-rE63ZeLIe1QOC1oQn9rIioeynmfPgHUvl7CoUwQpZe3odr3MLSAcmrlOcbhvZrMl'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.10-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.11-min.js.gz'; as.onload = function () { if (!window.amplitudeGTM.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/generated/amplitude-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-snippet.js index 415025114..a7c8aae1a 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-snippet.js @@ -30,12 +30,19 @@ }); }; }; + var proxyInstance = function proxyInstance(instance, fn, args) { + instance._q.push({ + name: fn, + args: Array.prototype.slice.call(args, 0) + }); + }; var proxyMain = function proxyMain(instance, fn, isPromise) { var args = arguments; instance[fn] = function () { if (isPromise) return { promise: new Promise(getPromiseResult(instance, fn, Array.prototype.slice.call(arguments))) }; + proxyInstance(instance, fn, Array.prototype.slice.call(arguments)); }; }; var setUpProxy = function setUpProxy(instance) { @@ -49,10 +56,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-lzuFHflPvaR2vlYBQ7PrzEFuQMvqJSUojIKGZfuNk/QK4Ins/DRKBFrO9KVvOpMY'; + as.integrity = 'sha384-sBSss0pKJEOvJthpmyRpY+A6ITpyXzAu5NsLzKC+IBw7JHJwlo442Afg8SWa4KOU'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.10-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.11-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/package.json b/packages/marketing-analytics-browser/package.json index 9c5cb464a..f0766f8e2 100644 --- a/packages/marketing-analytics-browser/package.json +++ b/packages/marketing-analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/marketing-analytics-browser", - "version": "1.0.10", + "version": "1.0.11", "description": "Official Amplitude SDK for Web and Marketing Analytics", "keywords": [ "analytics", @@ -43,7 +43,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.13.0", + "@amplitude/analytics-browser": "^1.13.1", "@amplitude/analytics-client-common": "^1.2.0", "@amplitude/analytics-core": "^1.2.3", "@amplitude/analytics-types": "^1.3.3", diff --git a/packages/marketing-analytics-browser/src/version.ts b/packages/marketing-analytics-browser/src/version.ts index 1533ffbdf..8b9f2796e 100644 --- a/packages/marketing-analytics-browser/src/version.ts +++ b/packages/marketing-analytics-browser/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.0.10'; +export const VERSION = '1.0.11'; diff --git a/packages/plugin-auto-tracking-browser/CHANGELOG.md b/packages/plugin-auto-tracking-browser/CHANGELOG.md index 6a2c5a072..069f50ee9 100644 --- a/packages/plugin-auto-tracking-browser/CHANGELOG.md +++ b/packages/plugin-auto-tracking-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-auto-tracking-browser@0.1.1...@amplitude/plugin-auto-tracking-browser@0.1.2) (2023-09-27) + +**Note:** Version bump only for package @amplitude/plugin-auto-tracking-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.1.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-auto-tracking-browser@0.1.0...@amplitude/plugin-auto-tracking-browser@0.1.1) (2023-09-05) **Note:** Version bump only for package @amplitude/plugin-auto-tracking-browser diff --git a/packages/plugin-auto-tracking-browser/package.json b/packages/plugin-auto-tracking-browser/package.json index 45ea19e85..cb67cae38 100644 --- a/packages/plugin-auto-tracking-browser/package.json +++ b/packages/plugin-auto-tracking-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-auto-tracking-browser", - "version": "0.1.1", + "version": "0.1.2", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index b4427c7b2..2c8d90b34 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.6.14](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.13...@amplitude/plugin-session-replay-browser@0.6.14) (2023-09-27) + +**Note:** Version bump only for package @amplitude/plugin-session-replay-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.6.13](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.12...@amplitude/plugin-session-replay-browser@0.6.13) (2023-09-21) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index a43358fb0..5a322406a 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.6.13", + "version": "0.6.14", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -40,7 +40,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "^0.2.4", + "@amplitude/session-replay-browser": "^0.2.5", "idb-keyval": "^6.2.1", "rrweb": "^2.0.0-alpha.11", "tslib": "^2.4.1" diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index a9eacf09b..4f50fdc3f 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.2.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.2.4...@amplitude/session-replay-browser@0.2.5) (2023-09-27) + +### Bug Fixes + +- **session replay:** opt out should take effect immediately + ([7b3b571](https://github.com/amplitude/Amplitude-TypeScript/commit/7b3b571e38a9bf968581cabbff50c6c1fff7251f)) +- **session replay:** test identity store false case and opt out in middle of recording + ([938b5d5](https://github.com/amplitude/Amplitude-TypeScript/commit/938b5d524780385bff04d5a51588407712d6db66)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.2.4](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.2.3...@amplitude/session-replay-browser@0.2.4) (2023-09-21) **Note:** Version bump only for package @amplitude/session-replay-browser diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index d4ea166d7..156a90781 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "0.2.4", + "version": "0.2.5", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From d4a1b33930bb8d20dd28a10ff9075854348b5272 Mon Sep 17 00:00:00 2001 From: Jesse Wang Date: Fri, 6 Oct 2023 09:39:38 -0700 Subject: [PATCH 175/214] feat(session replay): add metadata to headers --- .../package.json | 3 ++- .../src/version.ts | 1 + packages/session-replay-browser/package.json | 3 ++- .../session-replay-browser/src/constants.ts | 1 + .../session-replay-browser/src/helpers.ts | 5 +++++ .../src/session-replay.ts | 11 ++++++++++- .../session-replay-browser/src/verstion.ts | 1 + .../test/session-replay.test.ts | 19 +++++++++++++++++-- 8 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 packages/plugin-session-replay-browser/src/version.ts create mode 100644 packages/session-replay-browser/src/verstion.ts diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 5a322406a..4043b7a82 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -31,7 +31,8 @@ "publish": "node ../../scripts/publish/upload-to-s3.js", "test": "jest", "typecheck": "tsc -p ./tsconfig.json", - "version": "yarn add @amplitude/analytics-types@\">=1 <3\" @amplitude/analytics-client-common@\">=1 <3\" @amplitude/analytics-core@\">=1 <3\"" + "version": "yarn add @amplitude/analytics-types@\">=1 <3\" @amplitude/analytics-client-common@\">=1 <3\" @amplitude/analytics-core@\">=1 <3\"", + "version-file": "node -p \"'export const VERSION = \\'' + require('./package.json').version + '\\';'\" > src/version.ts" }, "bugs": { "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" diff --git a/packages/plugin-session-replay-browser/src/version.ts b/packages/plugin-session-replay-browser/src/version.ts new file mode 100644 index 000000000..4eb5ae9ae --- /dev/null +++ b/packages/plugin-session-replay-browser/src/version.ts @@ -0,0 +1 @@ +export const VERSION = '0.6.14'; diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index 156a90781..ac1c15ba3 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -31,7 +31,8 @@ "publish": "node ../../scripts/publish/upload-to-s3.js", "test": "jest", "typecheck": "tsc -p ./tsconfig.json", - "version": "yarn add @amplitude/analytics-types@\">=1 <3\" @amplitude/analytics-client-common@\">=1 <3\" @amplitude/analytics-core@\">=1 <3\"" + "version": "yarn add @amplitude/analytics-types@\">=1 <3\" @amplitude/analytics-client-common@\">=1 <3\" @amplitude/analytics-core@\">=1 <3\"", + "version-file": "node -p \"'export const VERSION = \\'' + require('./package.json').version + '\\';'\" > src/version.ts" }, "bugs": { "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" diff --git a/packages/session-replay-browser/src/constants.ts b/packages/session-replay-browser/src/constants.ts index a1350cff6..3d534f47a 100644 --- a/packages/session-replay-browser/src/constants.ts +++ b/packages/session-replay-browser/src/constants.ts @@ -6,6 +6,7 @@ export const DEFAULT_EVENT_PROPERTY_PREFIX = '[Amplitude]'; export const DEFAULT_SESSION_REPLAY_PROPERTY = `${DEFAULT_EVENT_PROPERTY_PREFIX} Session Recorded`; export const DEFAULT_SESSION_START_EVENT = 'session_start'; export const DEFAULT_SESSION_END_EVENT = 'session_end'; +export const DEFAULT_SAMPLE_RATE = 1; export const BLOCK_CLASS = 'amp-block'; export const MASK_TEXT_CLASS = 'amp-mask'; diff --git a/packages/session-replay-browser/src/helpers.ts b/packages/session-replay-browser/src/helpers.ts index 3070fb1ca..502aa301a 100644 --- a/packages/session-replay-browser/src/helpers.ts +++ b/packages/session-replay-browser/src/helpers.ts @@ -25,3 +25,8 @@ export const isSessionInSample = function (sessionId: number, sampleRate: number const mod = absHashMultiply % 100; return mod / 100 < sampleRate; }; + +export const getCurrentUrl = () => { + // eslint-disable-next-line no-restricted-globals + return typeof window !== 'undefined' ? window.location.href : ''; +}; diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index 912fd42d5..fdfd5c944 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -6,6 +6,7 @@ import { pack, record } from 'rrweb'; import { SessionReplayConfig } from './config'; import { BLOCK_CLASS, + DEFAULT_SAMPLE_RATE, DEFAULT_SESSION_REPLAY_PROPERTY, MASK_TEXT_CLASS, MAX_EVENT_LIST_SIZE_IN_BYTES, @@ -17,7 +18,7 @@ import { STORAGE_PREFIX, defaultSessionStore, } from './constants'; -import { isSessionInSample, maskInputFn } from './helpers'; +import { isSessionInSample, maskInputFn, getCurrentUrl } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, MISSING_API_KEY_MESSAGE, @@ -37,6 +38,7 @@ import { SessionReplayContext, SessionReplayOptions, } from './typings/session-replay'; +import { VERSION } from './verstion'; export class SessionReplay implements AmplitudeSessionReplay { name = '@amplitude/session-replay-browser'; @@ -384,6 +386,10 @@ export class SessionReplay implements AmplitudeSessionReplay { return this.completeRequest({ context, err: MISSING_DEVICE_ID_MESSAGE }); } + const url = getCurrentUrl(); + const version = VERSION; + const sampleRate = this.config?.sampleRate || DEFAULT_SAMPLE_RATE; + const urlParams = new URLSearchParams({ device_id: deviceId, session_id: `${context.sessionId}`, @@ -400,6 +406,9 @@ export class SessionReplay implements AmplitudeSessionReplay { 'Content-Type': 'application/json', Accept: '*/*', Authorization: `Bearer ${apiKey}`, + 'X-Client-Version': version, + 'X-Client-Url': url, + 'X-Client-Sample-Rate': `${sampleRate}`, }, body: JSON.stringify(payload), method: 'POST', diff --git a/packages/session-replay-browser/src/verstion.ts b/packages/session-replay-browser/src/verstion.ts new file mode 100644 index 000000000..8f7fb4cf8 --- /dev/null +++ b/packages/session-replay-browser/src/verstion.ts @@ -0,0 +1 @@ +export const VERSION = '0.2.5'; diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index d595190db..bb9647f88 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -8,6 +8,7 @@ import * as Helpers from '../src/helpers'; import { UNEXPECTED_ERROR_MESSAGE, getSuccessMessage } from '../src/messages'; import { SessionReplay } from '../src/session-replay'; import { IDBStore, RecordingStatus, SessionReplayConfig, SessionReplayOptions } from '../src/typings/session-replay'; +import { VERSION } from '../src/verstion'; jest.mock('idb-keyval'); type MockedIDBKeyVal = jest.Mocked; @@ -1054,7 +1055,14 @@ describe('SessionReplayPlugin', () => { 'https://api-sr.amplitude.com/sessions/v2/track?device_id=1a2b3c&session_id=123&seq_number=1', { body: JSON.stringify({ version: 1, events: [mockEventString] }), - headers: { Accept: '*/*', 'Content-Type': 'application/json', Authorization: 'Bearer static_key' }, + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + Authorization: 'Bearer static_key', + 'X-Client-Sample-Rate': '1', + 'X-Client-Url': window.location.href, + 'X-Client-Version': VERSION, + }, method: 'POST', }, ); @@ -1077,7 +1085,14 @@ describe('SessionReplayPlugin', () => { 'https://api-sr.eu.amplitude.com/sessions/v2/track?device_id=1a2b3c&session_id=123&seq_number=1', { body: JSON.stringify({ version: 1, events: [mockEventString] }), - headers: { Accept: '*/*', 'Content-Type': 'application/json', Authorization: 'Bearer static_key' }, + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + Authorization: 'Bearer static_key', + 'X-Client-Sample-Rate': '1', + 'X-Client-Url': window.location.href, + 'X-Client-Version': VERSION, + }, method: 'POST', }, ); From 1bfa2a21f1c8204acb31c9a03273f0c9ce2a9bdc Mon Sep 17 00:00:00 2001 From: Marvin Liu Date: Fri, 6 Oct 2023 15:23:39 -0700 Subject: [PATCH 176/214] feat: update auto tracking plugin (#589) * feat: update auto-tracking plugin * test: add auto-tracking tests * feat: update interface --- .../plugin-auto-tracking-browser/README.md | 35 +- .../jest.config.js | 4 +- .../plugin-auto-tracking-browser/package.json | 1 + .../src/auto-tracking-plugin.ts | 72 +++- .../src/constants.ts | 4 + .../src/helpers.ts | 58 ++++ .../src/libs/finder.ts | 322 ++++++++++++++++++ .../test/auto-tracking-plugin.test.ts | 149 +++++++- .../test/helpers.test.ts | 195 ++++++++++- .../test/setup.ts | 1 + yarn.lock | 5 + 11 files changed, 816 insertions(+), 30 deletions(-) create mode 100644 packages/plugin-auto-tracking-browser/src/libs/finder.ts create mode 100644 packages/plugin-auto-tracking-browser/test/setup.ts diff --git a/packages/plugin-auto-tracking-browser/README.md b/packages/plugin-auto-tracking-browser/README.md index 12838d5c7..0ff93b659 100644 --- a/packages/plugin-auto-tracking-browser/README.md +++ b/packages/plugin-auto-tracking-browser/README.md @@ -5,22 +5,23 @@

-# @amplitude/plugin-auto-tracking-browser (alpha) -**This plugin is in alpha stage, naming and interface might change in the future.** +# @amplitude/plugin-auto-tracking-browser (internal) +**This plugin is used internally only at the moment, naming and interface might change in the future.** Browser SDK plugin for auto-tracking. ## Installation +TBD. -This package is published on NPM registry and is available to be installed using npm and yarn. + -```sh -# npm -npm install @amplitude/plugin-auto-tracking-browser + + + -# yarn -yarn add @amplitude/plugin-auto-tracking-browser -``` + + + ## Usage @@ -44,8 +45,14 @@ The plugin accepts 1 optional parameter, which is an `Object` to configure the a ```typescript const plugin = autoTrackingPlugin({ - cssSelectorAllowlist: ['.amp-auto-tracking', '[amp-auto-tracking]'], - tagAllowlist: ['button', 'a'], + cssSelectorAllowlist: [ + '.amp-auto-tracking', + '[amp-auto-tracking]' + ], + pageUrlAllowlist: [ + 'https://amplitude.com', + new RegExp('https://amplitude.com/blog/*') + ], }); ``` @@ -53,14 +60,16 @@ Examples: - The above `cssSelectorAllowlist` will only allow tracking elements like: - `` - `Link` -- The above `tagAllowlist` will only allow `button` and `a` tags to be tracked. +- The above `pageUrlAllowlist` will only allow the elements on URL "https://amplitude.com" or any URL matching the "https://amplitude.com/blog/*" to be tracked #### Options |Name|Type|Default|Description| |-|-|-|-| |`cssSelectorAllowlist`|`string[]`|`undefined`| When provided, only allow elements matching any selector to be tracked. | -|`tagAllowlist`|`string[]`|`['a', 'button', 'input', 'select', 'textarea', 'label']`| Only allow elements with tag in this list to be tracked. | +|`pageUrlAllowlist`|`(string\|RegExp)[]`|`undefined`| When provided, only allow elements matching URLs to be tracked. | +|`shouldTrackEventResolver`|`(actionType: ActionType, element: Element) => boolean`|`undefined`| When provided, overwrite the default filter behavior. | +|`dataAttributePrefix`|`string`|`'data-amp-auto-track-'`| Allow data attributes to be collected in event property. | ### 3. Install plugin to Amplitude SDK diff --git a/packages/plugin-auto-tracking-browser/jest.config.js b/packages/plugin-auto-tracking-browser/jest.config.js index a0ef09957..75aab20fd 100644 --- a/packages/plugin-auto-tracking-browser/jest.config.js +++ b/packages/plugin-auto-tracking-browser/jest.config.js @@ -7,4 +7,6 @@ module.exports = { rootDir: '.', testEnvironment: 'jsdom', coveragePathIgnorePatterns: ['index.ts'], -}; \ No newline at end of file + setupFiles: ['./test/setup.ts'], +}; + diff --git a/packages/plugin-auto-tracking-browser/package.json b/packages/plugin-auto-tracking-browser/package.json index cb67cae38..ca8af774d 100644 --- a/packages/plugin-auto-tracking-browser/package.json +++ b/packages/plugin-auto-tracking-browser/package.json @@ -46,6 +46,7 @@ "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^10.0.1", + "css.escape": "^1.5.1", "rollup": "^2.79.1", "rollup-plugin-execute": "^1.1.1", "rollup-plugin-gzip": "^3.1.0", diff --git a/packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts b/packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts index b3a12a45c..87b1888ef 100644 --- a/packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts +++ b/packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts @@ -1,12 +1,14 @@ /* eslint-disable no-restricted-globals */ import { BrowserClient, BrowserConfig, EnrichmentPlugin } from '@amplitude/analytics-types'; import * as constants from './constants'; -import { getText } from './helpers'; +import { getText, isPageUrlAllowed, getAttributesWithPrefix, removeEmptyProperties, getNearestLabel } from './helpers'; +import { finder } from './libs/finder'; type BrowserEnrichmentPlugin = EnrichmentPlugin; type ActionType = 'click' | 'change'; const DEFAULT_TAG_ALLOWLIST = ['a', 'button', 'input', 'select', 'textarea', 'label']; +const DEFAULT_DATA_ATTRIBUTE_PREFIX = 'data-amp-auto-track-'; interface EventListener { element: Element; @@ -15,12 +17,44 @@ interface EventListener { } interface Options { + /** + * List of CSS selectors to allow auto tracking on. + * When provided, allow elements matching any selector to be tracked. + * Only the ['a', 'button', 'input', 'select', 'textarea', 'label'] tags are tracked regardless the selector provided here. + */ cssSelectorAllowlist?: string[]; - tagAllowlist?: string[]; + + /** + * List of page URLs to allow auto tracking on. + * When provided, only allow tracking on these URLs. + * Both full URLs and regex are supported. + */ + pageUrlAllowlist?: (string | RegExp)[]; + + /** + * Function to determine whether an event should be tracked. + * When provided, this function overwrites all other allowlist configurations. + * If the function returns true, the event will be tracked. + * If the function returns false, the event will not be tracked. + * @param actionType - The type of action that triggered the event. + * @param element - The element that triggered the event. + */ + shouldTrackEventResolver?: (actionType: ActionType, element: Element) => boolean; + + /** + * Prefix for data attributes to allow auto collecting. + * Default is 'data-amp-auto-track-'. + */ + dataAttributePrefix?: string; } export const autoTrackingPlugin = (options: Options = {}): BrowserEnrichmentPlugin => { - const { tagAllowlist = DEFAULT_TAG_ALLOWLIST, cssSelectorAllowlist } = options; + const { + cssSelectorAllowlist, + pageUrlAllowlist, + shouldTrackEventResolver, + dataAttributePrefix = DEFAULT_DATA_ATTRIBUTE_PREFIX, + } = options; const name = constants.PLUGIN_NAME; const type = 'enrichment'; @@ -52,6 +86,15 @@ export const autoTrackingPlugin = (options: Options = {}): BrowserEnrichmentPlug if (!element) { return false; } + + if (shouldTrackEventResolver) { + return shouldTrackEventResolver(actionType, element); + } + + if (!isPageUrlAllowed(window.location.href, pageUrlAllowlist)) { + return false; + } + /* istanbul ignore next */ const elementType = (element as HTMLInputElement)?.type || ''; if (typeof elementType === 'string') { @@ -62,11 +105,16 @@ export const autoTrackingPlugin = (options: Options = {}): BrowserEnrichmentPlug return false; } } - const tag = element.tagName.toLowerCase(); + /* istanbul ignore next */ + const tag = element?.tagName?.toLowerCase?.(); + + // We only track these limited tags, as general text tag is not interactive and might contain sensitive information. /* istanbul ignore if */ - if (!tagAllowlist.includes(tag)) { + if (!DEFAULT_TAG_ALLOWLIST.includes(tag)) { return false; } + + /* istanbul ignore if */ if (cssSelectorAllowlist) { const hasMatchAnyAllowedSelector = cssSelectorAllowlist.some((selector) => element.matches(selector)); if (!hasMatchAnyAllowedSelector) { @@ -95,6 +143,10 @@ export const autoTrackingPlugin = (options: Options = {}): BrowserEnrichmentPlug /* istanbul ignore next */ const rect = typeof element.getBoundingClientRect === 'function' ? element.getBoundingClientRect() : { left: null, top: null }; + const ariaLabel = element.getAttribute('aria-label'); + const attributes = getAttributesWithPrefix(element, dataAttributePrefix); + const nearestLabel = getNearestLabel(element); + const selector = finder(element); /* istanbul ignore next */ const properties: Record = { [constants.AMPLITUDE_EVENT_PROP_ELEMENT_ID]: element.id, @@ -103,6 +155,10 @@ export const autoTrackingPlugin = (options: Options = {}): BrowserEnrichmentPlug [constants.AMPLITUDE_EVENT_PROP_ELEMENT_TEXT]: getText(element), [constants.AMPLITUDE_EVENT_PROP_ELEMENT_POSITION_LEFT]: rect.left == null ? null : Math.round(rect.left), [constants.AMPLITUDE_EVENT_PROP_ELEMENT_POSITION_TOP]: rect.top == null ? null : Math.round(rect.top), + [constants.AMPLITUDE_EVENT_PROP_ELEMENT_ARIA_LABEL]: ariaLabel, + [constants.AMPLITUDE_EVENT_PROP_ELEMENT_ATTRIBUTES]: attributes, + [constants.AMPLITUDE_EVENT_PROP_ELEMENT_SELECTOR]: selector, + [constants.AMPLITUDE_EVENT_PROP_ELEMENT_PARENT_LABEL]: nearestLabel, [constants.AMPLITUDE_EVENT_PROP_PAGE_URL]: window.location.href.split('?')[0], [constants.AMPLITUDE_EVENT_PROP_PAGE_TITLE]: (typeof document !== 'undefined' && document.title) || '', [constants.AMPLITUDE_EVENT_PROP_VIEWPORT_HEIGHT]: window.innerHeight, @@ -111,7 +167,7 @@ export const autoTrackingPlugin = (options: Options = {}): BrowserEnrichmentPlug if (tag === 'a' && actionType === 'click' && element instanceof HTMLAnchorElement) { properties[constants.AMPLITUDE_EVENT_PROP_ELEMENT_HREF] = element.href; } - return properties; + return removeEmptyProperties(properties); }; const setup: BrowserEnrichmentPlugin['setup'] = async (config, amplitude) => { @@ -140,7 +196,7 @@ export const autoTrackingPlugin = (options: Options = {}): BrowserEnrichmentPlug }); } }; - const allElements = Array.from(document.body.querySelectorAll(tagAllowlist.join(','))); + const allElements = Array.from(document.body.querySelectorAll(DEFAULT_TAG_ALLOWLIST.join(','))); allElements.forEach(addListener); if (typeof MutationObserver !== 'undefined') { observer = new MutationObserver((mutations) => { @@ -148,7 +204,7 @@ export const autoTrackingPlugin = (options: Options = {}): BrowserEnrichmentPlug mutation.addedNodes.forEach((node: Node) => { addListener(node as Element); if ('querySelectorAll' in node && typeof node.querySelectorAll === 'function') { - Array.from(node.querySelectorAll(tagAllowlist.join(',')) as HTMLElement[]).map(addListener); + Array.from(node.querySelectorAll(DEFAULT_TAG_ALLOWLIST.join(',')) as HTMLElement[]).map(addListener); } }); }); diff --git a/packages/plugin-auto-tracking-browser/src/constants.ts b/packages/plugin-auto-tracking-browser/src/constants.ts index 755c03193..3cb44815d 100644 --- a/packages/plugin-auto-tracking-browser/src/constants.ts +++ b/packages/plugin-auto-tracking-browser/src/constants.ts @@ -10,6 +10,10 @@ export const AMPLITUDE_EVENT_PROP_ELEMENT_TEXT = '[Amplitude] Element Text'; export const AMPLITUDE_EVENT_PROP_ELEMENT_HREF = '[Amplitude] Element Href'; export const AMPLITUDE_EVENT_PROP_ELEMENT_POSITION_LEFT = '[Amplitude] Element Position Left'; export const AMPLITUDE_EVENT_PROP_ELEMENT_POSITION_TOP = '[Amplitude] Element Position Top'; +export const AMPLITUDE_EVENT_PROP_ELEMENT_ARIA_LABEL = '[Amplitude] Element Aria Label'; +export const AMPLITUDE_EVENT_PROP_ELEMENT_ATTRIBUTES = '[Amplitude] Element Attributes'; +export const AMPLITUDE_EVENT_PROP_ELEMENT_SELECTOR = '[Amplitude] Element Selector'; +export const AMPLITUDE_EVENT_PROP_ELEMENT_PARENT_LABEL = '[Amplitude] Element Parent Label'; export const AMPLITUDE_EVENT_PROP_PAGE_URL = '[Amplitude] Page URL'; export const AMPLITUDE_EVENT_PROP_PAGE_TITLE = '[Amplitude] Page Title'; export const AMPLITUDE_EVENT_PROP_VIEWPORT_HEIGHT = '[Amplitude] Viewport Height'; diff --git a/packages/plugin-auto-tracking-browser/src/helpers.ts b/packages/plugin-auto-tracking-browser/src/helpers.ts index 738d17c48..2a355a292 100644 --- a/packages/plugin-auto-tracking-browser/src/helpers.ts +++ b/packages/plugin-auto-tracking-browser/src/helpers.ts @@ -51,3 +51,61 @@ export const getText = (element: Element): string => { } return text; }; + +export const isPageUrlAllowed = (url: string, pageUrlAllowlist: (string | RegExp)[] | undefined) => { + if (!pageUrlAllowlist || !pageUrlAllowlist.length) { + return true; + } + return pageUrlAllowlist.some((allowedUrl) => { + if (typeof allowedUrl === 'string') { + return url === allowedUrl; + } + return url.match(allowedUrl); + }); +}; + +export const getAttributesWithPrefix = (element: Element, prefix: string): { [key: string]: string } => { + return element.getAttributeNames().reduce((attributes: { [key: string]: string }, attributeName) => { + if (attributeName.startsWith(prefix)) { + const attributeKey = attributeName.replace(prefix, ''); + const attributeValue = element.getAttribute(attributeName); + if (attributeKey) { + attributes[attributeKey] = attributeValue || ''; + } + } + return attributes; + }, {}); +}; + +export const isEmpty = (value: unknown) => { + return ( + value === undefined || + value === null || + (typeof value === 'object' && Object.keys(value).length === 0) || + (typeof value === 'string' && value.trim().length === 0) + ); +}; + +export const removeEmptyProperties = (properties: { [key: string]: unknown }) => { + return Object.keys(properties).reduce((filteredProperties: { [key: string]: unknown }, key) => { + const value = properties[key]; + if (!isEmpty(value)) { + filteredProperties[key] = value; + } + return filteredProperties; + }, {}); +}; + +export const getNearestLabel = (element: Element): string => { + const parent = element.parentElement; + if (!parent) { + return ''; + } + const labelElement = parent.querySelector(':scope>span,h1,h2,h3,h4,h5,h6'); + if (labelElement) { + /* istanbul ignore next */ + const labelText = labelElement.textContent || ''; + return isNonSensitiveString(labelText) ? labelText : ''; + } + return getNearestLabel(parent); +}; diff --git a/packages/plugin-auto-tracking-browser/src/libs/finder.ts b/packages/plugin-auto-tracking-browser/src/libs/finder.ts new file mode 100644 index 000000000..cbaa89bc6 --- /dev/null +++ b/packages/plugin-auto-tracking-browser/src/libs/finder.ts @@ -0,0 +1,322 @@ +/* istanbul ignore file */ + +// License: MIT +// Author: Anton Medvedev +// Source: https://github.com/antonmedv/finder + +type Knot = { + name: string; + penalty: number; + level?: number; +}; + +type Path = Knot[]; + +export type Options = { + root: Element; + idName: (name: string) => boolean; + className: (name: string) => boolean; + tagName: (name: string) => boolean; + attr: (name: string, value: string) => boolean; + seedMinLength: number; + optimizedMinLength: number; + threshold: number; + maxNumberOfTries: number; +}; + +let config: Options; +let rootDocument: Document | Element; + +export function finder(input: Element, options?: Partial) { + if (input.nodeType !== Node.ELEMENT_NODE) { + throw new Error(`Can't generate CSS selector for non-element node type.`); + } + if ('html' === input.tagName.toLowerCase()) { + return 'html'; + } + const defaults: Options = { + root: document.body, + idName: (_name: string) => true, + className: (_name: string) => true, + tagName: (_name: string) => true, + attr: (_name: string, _value: string) => false, + seedMinLength: 1, + optimizedMinLength: 2, + threshold: 1000, + maxNumberOfTries: 10000, + }; + + config = { ...defaults, ...options }; + rootDocument = findRootDocument(config.root, defaults); + + let path = bottomUpSearch(input, 'all', () => + bottomUpSearch(input, 'two', () => bottomUpSearch(input, 'one', () => bottomUpSearch(input, 'none'))), + ); + + if (path) { + const optimized = sort(optimize(path, input)); + if (optimized.length > 0) { + path = optimized[0]; + } + return selector(path); + } else { + throw new Error(`Selector was not found.`); + } +} + +function findRootDocument(rootNode: Element | Document, defaults: Options) { + if (rootNode.nodeType === Node.DOCUMENT_NODE) { + return rootNode; + } + if (rootNode === defaults.root) { + return rootNode.ownerDocument; + } + return rootNode; +} + +function bottomUpSearch( + input: Element, + limit: 'all' | 'two' | 'one' | 'none', + fallback?: () => Path | null, +): Path | null { + let path: Path | null = null; + const stack: Knot[][] = []; + let current: Element | null = input; + let i = 0; + while (current) { + let level: Knot[] = maybe(id(current)) || + maybe(...attr(current)) || + maybe(...classNames(current)) || + maybe(tagName(current)) || [any()]; + const nth = index(current); + if (limit == 'all') { + if (nth) { + level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth))); + } + } else if (limit == 'two') { + level = level.slice(0, 1); + if (nth) { + level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth))); + } + } else if (limit == 'one') { + const [node] = (level = level.slice(0, 1)); + if (nth && dispensableNth(node)) { + level = [nthChild(node, nth)]; + } + } else if (limit == 'none') { + level = [any()]; + if (nth) { + level = [nthChild(level[0], nth)]; + } + } + for (const node of level) { + node.level = i; + } + stack.push(level); + if (stack.length >= config.seedMinLength) { + path = findUniquePath(stack, fallback); + if (path) { + break; + } + } + current = current.parentElement; + i++; + } + if (!path) { + path = findUniquePath(stack, fallback); + } + if (!path && fallback) { + return fallback(); + } + return path; +} + +function findUniquePath(stack: Knot[][], fallback?: () => Path | null): Path | null { + const paths = sort(combinations(stack)); + if (paths.length > config.threshold) { + return fallback ? fallback() : null; + } + for (const candidate of paths) { + if (unique(candidate)) { + return candidate; + } + } + return null; +} + +function selector(path: Path): string { + let node = path[0]; + let query = node.name; + for (let i = 1; i < path.length; i++) { + const level = path[i].level || 0; + if (node.level === level - 1) { + query = `${path[i].name} > ${query}`; + } else { + query = `${path[i].name} ${query}`; + } + node = path[i]; + } + return query; +} + +function penalty(path: Path): number { + return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0); +} + +function unique(path: Path) { + const css = selector(path); + switch (rootDocument.querySelectorAll(css).length) { + case 0: + throw new Error(`Can't select any node with this selector: ${css}`); + case 1: + return true; + default: + return false; + } +} + +function id(input: Element): Knot | null { + const elementId = input.getAttribute('id'); + if (elementId && config.idName(elementId)) { + return { + name: '#' + CSS.escape(elementId), + penalty: 0, + }; + } + return null; +} + +function attr(input: Element): Knot[] { + const attrs = Array.from(input.attributes).filter((attr) => config.attr(attr.name, attr.value)); + return attrs.map( + (attr): Knot => ({ + name: `[${CSS.escape(attr.name)}="${CSS.escape(attr.value)}"]`, + penalty: 0.5, + }), + ); +} + +function classNames(input: Element): Knot[] { + const names = Array.from(input.classList).filter(config.className); + return names.map( + (name): Knot => ({ + name: '.' + CSS.escape(name), + penalty: 1, + }), + ); +} + +function tagName(input: Element): Knot | null { + const name = input.tagName.toLowerCase(); + if (config.tagName(name)) { + return { + name, + penalty: 2, + }; + } + return null; +} + +function any(): Knot { + return { + name: '*', + penalty: 3, + }; +} + +function index(input: Element): number | null { + const parent = input.parentNode; + if (!parent) { + return null; + } + let child = parent.firstChild; + if (!child) { + return null; + } + let i = 0; + while (child) { + if (child.nodeType === Node.ELEMENT_NODE) { + i++; + } + if (child === input) { + break; + } + child = child.nextSibling; + } + return i; +} + +function nthChild(node: Knot, i: number): Knot { + return { + name: node.name + `:nth-child(${i})`, + penalty: node.penalty + 1, + }; +} + +function dispensableNth(node: Knot) { + return node.name !== 'html' && !node.name.startsWith('#'); +} + +function maybe(...level: (Knot | null)[]): Knot[] | null { + const list = level.filter(notEmpty); + if (list.length > 0) { + return list; + } + return null; +} + +function notEmpty(value: T | null | undefined): value is T { + return value !== null && value !== undefined; +} + +function* combinations(stack: Knot[][], path: Knot[] = []): Generator { + if (stack.length > 0) { + for (const node of stack[0]) { + yield* combinations(stack.slice(1, stack.length), path.concat(node)); + } + } else { + yield path; + } +} + +function sort(paths: Iterable): Path[] { + return [...paths].sort((a, b) => penalty(a) - penalty(b)); +} + +type Scope = { + counter: number; + visited: Map; +}; + +function* optimize( + path: Path, + input: Element, + scope: Scope = { + counter: 0, + visited: new Map(), + }, +): Generator { + if (path.length > 2 && path.length > config.optimizedMinLength) { + for (let i = 1; i < path.length - 1; i++) { + if (scope.counter > config.maxNumberOfTries) { + return; // Okay At least I tried! + } + scope.counter += 1; + const newPath = [...path]; + newPath.splice(i, 1); + const newPathKey = selector(newPath); + if (scope.visited.has(newPathKey)) { + return; + } + if (unique(newPath) && same(newPath, input)) { + yield newPath; + scope.visited.set(newPathKey, true); + yield* optimize(newPath, input, scope); + } + } + } +} + +function same(path: Path, input: Element) { + return rootDocument.querySelector(selector(path)) === input; +} diff --git a/packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts b/packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts index 1cec8f95d..4cb765e79 100644 --- a/packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts +++ b/packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts @@ -115,10 +115,15 @@ describe('autoTrackingPlugin', () => { const link = document.createElement('a'); link.setAttribute('id', 'my-link-id'); link.setAttribute('class', 'my-link-class'); + link.setAttribute('aria-label', 'my-link'); link.href = 'https://www.amplitude.com/click-link'; link.text = 'my-link-text'; document.body.appendChild(link); + const h2 = document.createElement('h2'); + h2.textContent = 'my-h2-text'; + document.body.appendChild(h2); + mockWindowLocationFromURL(new URL('https://www.amplitude.com/unit-test?query=param')); }); @@ -151,7 +156,9 @@ describe('autoTrackingPlugin', () => { '[Amplitude] Element Position Top': 0, '[Amplitude] Element Tag': 'a', '[Amplitude] Element Text': 'my-link-text', - '[Amplitude] Page Title': '', + '[Amplitude] Element Aria Label': 'my-link', + '[Amplitude] Element Selector': '#my-link-id', + '[Amplitude] Element Parent Label': 'my-h2-text', '[Amplitude] Page URL': 'https://www.amplitude.com/unit-test', '[Amplitude] Viewport Height': 768, '[Amplitude] Viewport Width': 1024, @@ -183,6 +190,7 @@ describe('autoTrackingPlugin', () => { const buttonText = document.createTextNode('submit'); button.setAttribute('id', 'my-button-id'); button.setAttribute('class', 'my-button-class'); + button.setAttribute('aria-label', 'my-button'); button.appendChild(buttonText); document.body.appendChild(button); // allow mutation observer to execute and event listener to be attached @@ -197,7 +205,9 @@ describe('autoTrackingPlugin', () => { '[Amplitude] Element Position Top': 0, '[Amplitude] Element Tag': 'button', '[Amplitude] Element Text': 'submit', - '[Amplitude] Page Title': '', + '[Amplitude] Element Aria Label': 'my-button', + '[Amplitude] Element Selector': '#my-button-id', + '[Amplitude] Element Parent Label': 'my-h2-text', '[Amplitude] Page URL': 'https://www.amplitude.com/unit-test', '[Amplitude] Viewport Height': 768, '[Amplitude] Viewport Width': 1024, @@ -213,8 +223,12 @@ describe('autoTrackingPlugin', () => { expect(track).toHaveBeenCalledTimes(1); }); - test('should follow tagAllowlist configuration', async () => { - plugin = autoTrackingPlugin({ tagAllowlist: ['button'] }); + test('should not track disallowed tag', async () => { + const div = document.createElement('div'); + div.setAttribute('id', 'my-div-id'); + document.body.appendChild(div); + + plugin = autoTrackingPlugin(); const loggerProvider: Partial = { log: jest.fn(), warn: jest.fn(), @@ -225,10 +239,13 @@ describe('autoTrackingPlugin', () => { }; await plugin?.setup(config as BrowserConfig, instance); - // trigger click event - document.getElementById('my-link-id')?.dispatchEvent(new Event('click')); - + // trigger click div + document.getElementById('my-div-id')?.dispatchEvent(new Event('click')); expect(track).toHaveBeenCalledTimes(0); + + // trigger click link + document.getElementById('my-link-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(1); }); test('should follow cssSelectorAllowlist configuration', async () => { @@ -259,6 +276,124 @@ describe('autoTrackingPlugin', () => { expect(track).toHaveBeenCalledTimes(1); }); + test('should follow pageUrlAllowlist configuration', async () => { + plugin = autoTrackingPlugin({ pageUrlAllowlist: [new RegExp('https://www.test.com')] }); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click link + document.getElementById('my-link-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(0); + + // update current page url to match allowlist + mockWindowLocationFromURL(new URL('https://www.test.com/abc?query=param')); + const link = document.createElement('a'); + link.setAttribute('id', 'my-link-id-new-url'); + link.setAttribute('class', 'my-link-class'); + link.setAttribute('aria-label', 'my-link'); + link.href = 'https://www.amplitude.com/test'; + link.text = 'my-link-text'; + document.body.appendChild(link); + // allow mutation observer to execute and event listener to be attached + await new Promise((r) => r(undefined)); // basically, await next clock tick + + // trigger click link + document.getElementById('my-link-id-new-url')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(1); + }); + + test('should follow shouldTrackEventResolver configuration', async () => { + const button1 = document.createElement('button'); + const buttonText1 = document.createTextNode('submit'); + button1.setAttribute('id', 'my-button-id-1'); + button1.setAttribute('class', 'my-button-class'); + button1.appendChild(buttonText1); + document.body.appendChild(button1); + + const button2 = document.createElement('button'); + const buttonText2 = document.createTextNode('submit'); + button2.setAttribute('id', 'my-button-id-2'); + button2.setAttribute('class', 'my-button-class'); + button2.appendChild(buttonText2); + document.body.appendChild(button2); + + plugin = autoTrackingPlugin({ + shouldTrackEventResolver: (actionType, element) => + actionType === 'click' && element.id === 'my-button-id-1' && element.tagName === 'BUTTON', + }); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click button2 + document.getElementById('my-button-id-2')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(0); + + // trigger click button1 + document.getElementById('my-button-id-1')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(1); + }); + + test('should follow dataAttributePrefix configuration', async () => { + const button = document.createElement('button'); + const buttonText = document.createTextNode('submit'); + button.setAttribute('id', 'my-button-id'); + button.setAttribute('class', 'my-button-class'); + button.setAttribute('data-amp-test-hello', 'world'); + button.setAttribute('data-amp-test-time', 'machine'); + button.setAttribute('data-amp-test-test', ''); + button.appendChild(buttonText); + document.body.appendChild(button); + + plugin = autoTrackingPlugin({ + dataAttributePrefix: 'data-amp-test-', + }); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click button + document.getElementById('my-button-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(1); + expect(track).toHaveBeenNthCalledWith(1, '[Amplitude] Element Clicked', { + '[Amplitude] Element Class': 'my-button-class', + '[Amplitude] Element ID': 'my-button-id', + '[Amplitude] Element Position Left': 0, + '[Amplitude] Element Position Top': 0, + '[Amplitude] Element Tag': 'button', + '[Amplitude] Element Text': 'submit', + '[Amplitude] Element Selector': '#my-button-id', + '[Amplitude] Element Parent Label': 'my-h2-text', + '[Amplitude] Page URL': 'https://www.amplitude.com/unit-test', + '[Amplitude] Viewport Height': 768, + '[Amplitude] Viewport Width': 1024, + '[Amplitude] Element Attributes': { + hello: 'world', + time: 'machine', + test: '', + }, + }); + }); + test('should track change event', async () => { const input = document.createElement('input'); input.setAttribute('id', 'my-input-id'); diff --git a/packages/plugin-auto-tracking-browser/test/helpers.test.ts b/packages/plugin-auto-tracking-browser/test/helpers.test.ts index b8d92c4bf..466edc086 100644 --- a/packages/plugin-auto-tracking-browser/test/helpers.test.ts +++ b/packages/plugin-auto-tracking-browser/test/helpers.test.ts @@ -1,4 +1,14 @@ -import { isNonSensitiveString, isTextNode, isNonSensitiveElement, getText } from '../src/helpers'; +import { + isNonSensitiveString, + isTextNode, + isNonSensitiveElement, + getText, + isPageUrlAllowed, + getAttributesWithPrefix, + isEmpty, + removeEmptyProperties, + getNearestLabel, +} from '../src/helpers'; describe('autoTrackingPlugin helpers', () => { describe('isNonSensitiveString', () => { @@ -123,4 +133,187 @@ describe('autoTrackingPlugin helpers', () => { expect(result).toEqual('submit and pay'); }); }); + + describe('isPageUrlAllowed', () => { + const url = 'https://amplitude.com/blog'; + + test('should return true when allow list is not provided', () => { + const result = isPageUrlAllowed(url, undefined); + expect(result).toEqual(true); + }); + + test('should return true when allow list is empty', () => { + const result = isPageUrlAllowed(url, []); + expect(result).toEqual(true); + }); + + test('should return true only when full url string is in the allow list', () => { + let result = isPageUrlAllowed(url, ['https://amplitude.com/blog']); + expect(result).toEqual(true); + + result = isPageUrlAllowed('https://amplitude.com/market', ['https://amplitude.com/blog']); + expect(result).toEqual(false); + }); + + test('should return true when url regex is in the allow list', () => { + let result = isPageUrlAllowed(url, [new RegExp('https://amplitude.com/')]); + expect(result).toEqual(true); + + result = isPageUrlAllowed('https://amplitude.com/market', [new RegExp('https://amplitude.com/')]); + expect(result).toEqual(true); + }); + + test('should return false when url is not in the allow list at all', () => { + const result = isPageUrlAllowed(url, ['https://test.com', new RegExp('https://test.com/')]); + expect(result).toEqual(false); + }); + + test('should return true when url is matching an item in the allow list with regex wildcard', () => { + const result = isPageUrlAllowed(url, [new RegExp('http.?://amplitude.*'), new RegExp('http.?://test.*')]); + expect(result).toEqual(true); + }); + }); + + describe('getAttributesWithPrefix', () => { + test('should return attributes when matching the prefix', () => { + const element = document.createElement('input'); + element.setAttribute('data-amp-auto-track-hello', 'world'); + element.setAttribute('data-amp-auto-track-time', 'machine'); + element.setAttribute('data-amp-auto-track-test', ''); + const result = getAttributesWithPrefix(element, 'data-amp-auto-track-'); + expect(result).toEqual({ hello: 'world', time: 'machine', test: '' }); + }); + + test('should return empty attributes when no attribute name matching the prefix', () => { + const element = document.createElement('input'); + element.setAttribute('data-hello', 'world'); + element.setAttribute('data-time', 'machine'); + const result = getAttributesWithPrefix(element, 'data-amp-auto-track-'); + expect(result).toEqual({}); + }); + + test('should return all attributes when prefix is empty string', () => { + const element = document.createElement('input'); + element.setAttribute('data-hello', 'world'); + element.setAttribute('data-time', 'machine'); + const result = getAttributesWithPrefix(element, ''); + expect(result).toEqual({ 'data-hello': 'world', 'data-time': 'machine' }); + }); + }); + + describe('isEmpty', () => { + test('should return true when value is undefined', () => { + const result = isEmpty(undefined); + expect(result).toEqual(true); + }); + + test('should return true when value is null', () => { + const result = isEmpty(null); + expect(result).toEqual(true); + }); + + test('should return true when value is empty array', () => { + const result = isEmpty([]); + expect(result).toEqual(true); + }); + + test('should return true when value is empty object', () => { + const result = isEmpty({}); + expect(result).toEqual(true); + }); + + test('should return true when value is empty string', () => { + const result = isEmpty(''); + expect(result).toEqual(true); + }); + + test('should return true when value is string with spaces only', () => { + const result = isEmpty(' '); + expect(result).toEqual(true); + }); + + test('should return false when value is array', () => { + const result = isEmpty([1, 2]); + expect(result).toEqual(false); + }); + + test('should return false when value is object', () => { + const result = isEmpty({ x: 1 }); + expect(result).toEqual(false); + }); + + test('should return false when value is string', () => { + const result = isEmpty('xxx'); + expect(result).toEqual(false); + }); + }); + + describe('removeEmptyProperties', () => { + test('should filter out empty properties', () => { + const result = removeEmptyProperties({ + x: 1, + y: [1], + z: { z: 1 }, + w: 'w', + a: undefined, + b: [], + c: {}, + d: ' ', + e: null, + }); + expect(result).toEqual({ + x: 1, + y: [1], + z: { z: 1 }, + w: 'w', + }); + }); + }); + + describe('getNearestLabel', () => { + test('should return nearest label of the element', () => { + const div = document.createElement('div'); + const span = document.createElement('span'); + span.textContent = 'nearest label'; + const input = document.createElement('input'); + div.appendChild(span); + div.appendChild(input); + + const result = getNearestLabel(input); + expect(result).toEqual('nearest label'); + }); + + test('should return redacted nearest label when content is sensitive', () => { + const div = document.createElement('div'); + const span = document.createElement('span'); + span.textContent = '4916024123820164'; + const input = document.createElement('input'); + div.appendChild(span); + div.appendChild(input); + + const result = getNearestLabel(input); + expect(result).toEqual(''); + }); + + test('should return nearest label of the element parent', () => { + const div = document.createElement('div'); + const innerDiv = document.createElement('div'); + div.appendChild(innerDiv); + const span = document.createElement('span'); + span.textContent = 'parent label'; + div.appendChild(span); + const input = document.createElement('input'); + innerDiv.appendChild(input); + + const result = getNearestLabel(input); + expect(result).toEqual('parent label'); + }); + + test('should return empty string when there is no parent', () => { + const input = document.createElement('input'); + + const result = getNearestLabel(input); + expect(result).toEqual(''); + }); + }); }); diff --git a/packages/plugin-auto-tracking-browser/test/setup.ts b/packages/plugin-auto-tracking-browser/test/setup.ts new file mode 100644 index 000000000..df3654bdd --- /dev/null +++ b/packages/plugin-auto-tracking-browser/test/setup.ts @@ -0,0 +1 @@ +require('css.escape'); diff --git a/yarn.lock b/yarn.lock index 5147df12d..2a836bfa9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6005,6 +6005,11 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + cssom@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" From bc2afba178f70341894ee40d0dfbb829e5b16395 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Fri, 6 Oct 2023 22:40:10 +0000 Subject: [PATCH 177/214] chore(release): publish - @amplitude/plugin-auto-tracking-browser@0.2.0 --- packages/plugin-auto-tracking-browser/CHANGELOG.md | 12 ++++++++++++ packages/plugin-auto-tracking-browser/package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/plugin-auto-tracking-browser/CHANGELOG.md b/packages/plugin-auto-tracking-browser/CHANGELOG.md index 069f50ee9..916322083 100644 --- a/packages/plugin-auto-tracking-browser/CHANGELOG.md +++ b/packages/plugin-auto-tracking-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.2.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-auto-tracking-browser@0.1.2...@amplitude/plugin-auto-tracking-browser@0.2.0) (2023-10-06) + +### Features + +- update auto tracking plugin ([#589](https://github.com/amplitude/Amplitude-TypeScript/issues/589)) + ([1bfa2a2](https://github.com/amplitude/Amplitude-TypeScript/commit/1bfa2a21f1c8204acb31c9a03273f0c9ce2a9bdc)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.1.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-auto-tracking-browser@0.1.1...@amplitude/plugin-auto-tracking-browser@0.1.2) (2023-09-27) **Note:** Version bump only for package @amplitude/plugin-auto-tracking-browser diff --git a/packages/plugin-auto-tracking-browser/package.json b/packages/plugin-auto-tracking-browser/package.json index ca8af774d..bb86a8b45 100644 --- a/packages/plugin-auto-tracking-browser/package.json +++ b/packages/plugin-auto-tracking-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-auto-tracking-browser", - "version": "0.1.2", + "version": "0.2.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 6d15bfb5daccd52d1dc4f7ae1ead8d5f4b2bf6a2 Mon Sep 17 00:00:00 2001 From: Jesse Wang Date: Tue, 10 Oct 2023 21:54:58 -0700 Subject: [PATCH 178/214] feat(session replay): add metadata to headers --- packages/session-replay-browser/src/config.ts | 3 +- .../session-replay-browser/src/constants.ts | 2 +- .../src/session-replay.ts | 13 ++++-- .../src/{verstion.ts => version.ts} | 0 .../test/session-replay.test.ts | 43 +++++++++++++++++-- 5 files changed, 52 insertions(+), 9 deletions(-) rename packages/session-replay-browser/src/{verstion.ts => version.ts} (100%) diff --git a/packages/session-replay-browser/src/config.ts b/packages/session-replay-browser/src/config.ts index 4f6482d4e..8acd5dc28 100644 --- a/packages/session-replay-browser/src/config.ts +++ b/packages/session-replay-browser/src/config.ts @@ -2,6 +2,7 @@ import { FetchTransport } from '@amplitude/analytics-client-common'; import { Config, Logger } from '@amplitude/analytics-core'; import { LogLevel } from '@amplitude/analytics-types'; import { SessionReplayConfig as ISessionReplayConfig, SessionReplayOptions } from './typings/session-replay'; +import { DEFAULT_SAMPLE_RATE } from './constants'; export const getDefaultConfig = () => ({ flushMaxRetries: 2, @@ -29,7 +30,7 @@ export class SessionReplayConfig extends Config implements ISessionReplayConfig : defaultConfig.flushMaxRetries; this.apiKey = apiKey; - this.sampleRate = options.sampleRate || 1; + this.sampleRate = options.sampleRate || DEFAULT_SAMPLE_RATE; this.deviceId = options.deviceId; this.sessionId = options.sessionId; diff --git a/packages/session-replay-browser/src/constants.ts b/packages/session-replay-browser/src/constants.ts index 3d534f47a..415860376 100644 --- a/packages/session-replay-browser/src/constants.ts +++ b/packages/session-replay-browser/src/constants.ts @@ -6,7 +6,7 @@ export const DEFAULT_EVENT_PROPERTY_PREFIX = '[Amplitude]'; export const DEFAULT_SESSION_REPLAY_PROPERTY = `${DEFAULT_EVENT_PROPERTY_PREFIX} Session Recorded`; export const DEFAULT_SESSION_START_EVENT = 'session_start'; export const DEFAULT_SESSION_END_EVENT = 'session_end'; -export const DEFAULT_SAMPLE_RATE = 1; +export const DEFAULT_SAMPLE_RATE = 0; export const BLOCK_CLASS = 'amp-block'; export const MASK_TEXT_CLASS = 'amp-mask'; diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index fdfd5c944..e4aedd2f6 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -38,7 +38,7 @@ import { SessionReplayContext, SessionReplayOptions, } from './typings/session-replay'; -import { VERSION } from './verstion'; +import { VERSION } from './version'; export class SessionReplay implements AmplitudeSessionReplay { name = '@amplitude/session-replay-browser'; @@ -359,6 +359,10 @@ export class SessionReplay implements AmplitudeSessionReplay { await Promise.all(list.map((context) => this.send(context, useRetry))); } + getSampleRate() { + return this.config?.sampleRate || DEFAULT_SAMPLE_RATE; + } + getServerUrl() { if (this.config?.serverZone === ServerZone.EU) { return SESSION_REPLAY_EU_SERVER_URL; @@ -385,10 +389,12 @@ export class SessionReplay implements AmplitudeSessionReplay { if (!deviceId) { return this.completeRequest({ context, err: MISSING_DEVICE_ID_MESSAGE }); } - const url = getCurrentUrl(); const version = VERSION; - const sampleRate = this.config?.sampleRate || DEFAULT_SAMPLE_RATE; + const sampleRate = this.getSampleRate(); + + console.log('sampleRate', sampleRate); + console.log('config', this.config); const urlParams = new URLSearchParams({ device_id: deviceId, @@ -400,6 +406,7 @@ export class SessionReplay implements AmplitudeSessionReplay { version: 1, events: context.events, }; + try { const options: RequestInit = { headers: { diff --git a/packages/session-replay-browser/src/verstion.ts b/packages/session-replay-browser/src/version.ts similarity index 100% rename from packages/session-replay-browser/src/verstion.ts rename to packages/session-replay-browser/src/version.ts diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index bb9647f88..7f86f2929 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -3,12 +3,12 @@ import * as AnalyticsClientCommon from '@amplitude/analytics-client-common'; import { LogLevel, Logger, ServerZone } from '@amplitude/analytics-types'; import * as IDBKeyVal from 'idb-keyval'; import * as RRWeb from 'rrweb'; -import { DEFAULT_SESSION_REPLAY_PROPERTY, SESSION_REPLAY_SERVER_URL } from '../src/constants'; +import { DEFAULT_SAMPLE_RATE, DEFAULT_SESSION_REPLAY_PROPERTY, SESSION_REPLAY_SERVER_URL } from '../src/constants'; import * as Helpers from '../src/helpers'; import { UNEXPECTED_ERROR_MESSAGE, getSuccessMessage } from '../src/messages'; import { SessionReplay } from '../src/session-replay'; import { IDBStore, RecordingStatus, SessionReplayConfig, SessionReplayOptions } from '../src/typings/session-replay'; -import { VERSION } from '../src/verstion'; +import { VERSION } from '../src/version'; jest.mock('idb-keyval'); type MockedIDBKeyVal = jest.Mocked; @@ -38,6 +38,7 @@ describe('SessionReplayPlugin', () => { const { get, update } = IDBKeyVal as MockedIDBKeyVal; const { record } = RRWeb as MockedRRWeb; let originalFetch: typeof global.fetch; + let location: typeof global.window.location; const mockLoggerProvider: MockedLogger = { error: jest.fn(), log: jest.fn(), @@ -80,6 +81,7 @@ describe('SessionReplayPlugin', () => { jest.resetAllMocks(); jest.spyOn(global.Math, 'random').mockRestore(); global.fetch = originalFetch; + global.window.location = location; jest.useRealTimers(); }); describe('init', () => { @@ -255,7 +257,14 @@ describe('SessionReplayPlugin', () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, mockOptions).promise; sessionReplay.getShouldRecord = () => false; + const result = sessionReplay.getSessionRecordingProperties(); + expect(result).toEqual({}); + }); + test('should return an default sample rate if not set', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, mockOptions).promise; + sessionReplay.getShouldRecord = () => false; const result = sessionReplay.getSessionRecordingProperties(); expect(result).toEqual({}); }); @@ -1007,6 +1016,15 @@ describe('SessionReplayPlugin', () => { }); }); + describe('getSampleRate', () => { + test('should return undefined if no config set', () => { + const sessionReplay = new SessionReplay(); + sessionReplay.config = undefined; + const sampleRate = sessionReplay.getSampleRate(); + expect(sampleRate).toEqual(0); + }); + }); + describe('send', () => { test('should not send anything if api key not set', async () => { const sessionReplay = new SessionReplay(); @@ -1059,7 +1077,7 @@ describe('SessionReplayPlugin', () => { Accept: '*/*', 'Content-Type': 'application/json', Authorization: 'Bearer static_key', - 'X-Client-Sample-Rate': '1', + 'X-Client-Sample-Rate': `${DEFAULT_SAMPLE_RATE}`, 'X-Client-Url': window.location.href, 'X-Client-Version': VERSION, }, @@ -1089,7 +1107,7 @@ describe('SessionReplayPlugin', () => { Accept: '*/*', 'Content-Type': 'application/json', Authorization: 'Bearer static_key', - 'X-Client-Sample-Rate': '1', + 'X-Client-Sample-Rate': `${DEFAULT_SAMPLE_RATE}`, 'X-Client-Url': window.location.href, 'X-Client-Version': VERSION, }, @@ -1777,4 +1795,21 @@ describe('SessionReplayPlugin', () => { }); }); }); + + describe('getCurrentUrl', () => { + let windowSpy: jest.SpyInstance; + beforeEach(() => { + windowSpy = jest.spyOn(globalThis, 'window', 'get'); + }); + + afterEach(() => { + windowSpy.mockRestore(); + }); + + test('runs without error', () => { + windowSpy.mockImplementation(() => undefined); + const url = Helpers.getCurrentUrl(); + expect(url).toEqual(''); + }); + }); }); From 222920a52c34cce5f4b111f9f2768836b248dcdb Mon Sep 17 00:00:00 2001 From: Jesse Wang Date: Tue, 10 Oct 2023 21:55:35 -0700 Subject: [PATCH 179/214] feat(session replay): add metadata to headers --- packages/session-replay-browser/src/session-replay.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index e4aedd2f6..2954d464f 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -393,9 +393,6 @@ export class SessionReplay implements AmplitudeSessionReplay { const version = VERSION; const sampleRate = this.getSampleRate(); - console.log('sampleRate', sampleRate); - console.log('config', this.config); - const urlParams = new URLSearchParams({ device_id: deviceId, session_id: `${context.sessionId}`, From feb18ca26dff26c0673068b2e17efbea68622c54 Mon Sep 17 00:00:00 2001 From: Marvin Liu Date: Wed, 11 Oct 2023 09:24:02 -0700 Subject: [PATCH 180/214] refactor: rename to default-event-tracking-advanced plugin (#599) * refactor: rename to default-event-tracking-advanced plugin * refactor: reset det advanced package version * refactor: update output function name --- .../plugin-auto-tracking-browser/CHANGELOG.md | 46 ------------------- .../plugin-auto-tracking-browser/src/index.ts | 1 - .../CHANGELOG.md | 0 .../README.md | 37 ++++++++------- .../jest.config.js | 0 .../package.json | 8 ++-- .../rollup.config.js | 2 +- .../src/constants.ts | 2 +- ...default-event-tracking-advanced-plugin.ts} | 4 +- .../src/helpers.ts | 0 .../src/index.ts | 4 ++ .../src/libs/finder.ts | 0 .../default-event-tracking-advanced.test.ts} | 20 ++++---- .../test/helpers.test.ts | 12 ++--- .../test/setup.ts | 0 .../tsconfig.es5.json | 0 .../tsconfig.esm.json | 0 .../tsconfig.json | 0 18 files changed, 46 insertions(+), 90 deletions(-) delete mode 100644 packages/plugin-auto-tracking-browser/CHANGELOG.md delete mode 100644 packages/plugin-auto-tracking-browser/src/index.ts create mode 100644 packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md rename packages/{plugin-auto-tracking-browser => plugin-default-event-tracking-advanced-browser}/README.md (65%) rename packages/{plugin-auto-tracking-browser => plugin-default-event-tracking-advanced-browser}/jest.config.js (100%) rename packages/{plugin-auto-tracking-browser => plugin-default-event-tracking-advanced-browser}/package.json (92%) rename packages/{plugin-auto-tracking-browser => plugin-default-event-tracking-advanced-browser}/rollup.config.js (63%) rename packages/{plugin-auto-tracking-browser => plugin-default-event-tracking-advanced-browser}/src/constants.ts (94%) rename packages/{plugin-auto-tracking-browser/src/auto-tracking-plugin.ts => plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts} (98%) rename packages/{plugin-auto-tracking-browser => plugin-default-event-tracking-advanced-browser}/src/helpers.ts (100%) create mode 100644 packages/plugin-default-event-tracking-advanced-browser/src/index.ts rename packages/{plugin-auto-tracking-browser => plugin-default-event-tracking-advanced-browser}/src/libs/finder.ts (100%) rename packages/{plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts => plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts} (96%) rename packages/{plugin-auto-tracking-browser => plugin-default-event-tracking-advanced-browser}/test/helpers.test.ts (97%) rename packages/{plugin-auto-tracking-browser => plugin-default-event-tracking-advanced-browser}/test/setup.ts (100%) rename packages/{plugin-auto-tracking-browser => plugin-default-event-tracking-advanced-browser}/tsconfig.es5.json (100%) rename packages/{plugin-auto-tracking-browser => plugin-default-event-tracking-advanced-browser}/tsconfig.esm.json (100%) rename packages/{plugin-auto-tracking-browser => plugin-default-event-tracking-advanced-browser}/tsconfig.json (100%) diff --git a/packages/plugin-auto-tracking-browser/CHANGELOG.md b/packages/plugin-auto-tracking-browser/CHANGELOG.md deleted file mode 100644 index 916322083..000000000 --- a/packages/plugin-auto-tracking-browser/CHANGELOG.md +++ /dev/null @@ -1,46 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# [0.2.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-auto-tracking-browser@0.1.2...@amplitude/plugin-auto-tracking-browser@0.2.0) (2023-10-06) - -### Features - -- update auto tracking plugin ([#589](https://github.com/amplitude/Amplitude-TypeScript/issues/589)) - ([1bfa2a2](https://github.com/amplitude/Amplitude-TypeScript/commit/1bfa2a21f1c8204acb31c9a03273f0c9ce2a9bdc)) - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.1.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-auto-tracking-browser@0.1.1...@amplitude/plugin-auto-tracking-browser@0.1.2) (2023-09-27) - -**Note:** Version bump only for package @amplitude/plugin-auto-tracking-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.1.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-auto-tracking-browser@0.1.0...@amplitude/plugin-auto-tracking-browser@0.1.1) (2023-09-05) - -**Note:** Version bump only for package @amplitude/plugin-auto-tracking-browser - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# 0.1.0 (2023-08-31) - -### Features - -- add auto-tracking plugin ([#570](https://github.com/amplitude/Amplitude-TypeScript/issues/570)) - ([757032f](https://github.com/amplitude/Amplitude-TypeScript/commit/757032f5c0eeac4396f28163ad14958ea44e8ace)) - -# Change Log - -All notable changes to this project will be documented in this file. See -[Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/packages/plugin-auto-tracking-browser/src/index.ts b/packages/plugin-auto-tracking-browser/src/index.ts deleted file mode 100644 index 8a184b4f1..000000000 --- a/packages/plugin-auto-tracking-browser/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { autoTrackingPlugin as plugin, autoTrackingPlugin } from './auto-tracking-plugin'; diff --git a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugin-auto-tracking-browser/README.md b/packages/plugin-default-event-tracking-advanced-browser/README.md similarity index 65% rename from packages/plugin-auto-tracking-browser/README.md rename to packages/plugin-default-event-tracking-advanced-browser/README.md index 0ff93b659..61716fc85 100644 --- a/packages/plugin-auto-tracking-browser/README.md +++ b/packages/plugin-default-event-tracking-advanced-browser/README.md @@ -5,23 +5,22 @@

-# @amplitude/plugin-auto-tracking-browser (internal) -**This plugin is used internally only at the moment, naming and interface might change in the future.** +# @amplitude/plugin-default-event-tracking-advanced-browser (beta) +**This plugin is in beta at the moment, naming and interface might change in the future.** -Browser SDK plugin for auto-tracking. +Browser SDK plugin for default event tracking advanced. ## Installation -TBD. - +This package is published on NPM registry and is available to be installed using npm and yarn. - - - +```sh +# npm +npm install @amplitude/plugin-default-event-tracking-advanced-browser - - - +# yarn +yarn add @amplitude/plugin-default-event-tracking-advanced-browser +``` ## Usage @@ -32,11 +31,11 @@ To use this plugin, you need to install `@amplitude/analytics-browser` version ` ### 1. Import Amplitude packages * `@amplitude/analytics-browser` -* `@amplitude/plugin-auto-tracking-browser` +* `@amplitude/plugin-default-event-tracking-advanced-browser` ```typescript import * as amplitude from '@amplitude/analytics-browser'; -import { autoTrackingPlugin } from '@amplitude/plugin-auto-tracking-browser'; +import { defaultEventTrackingAdvancedPlugin } from '@amplitude/plugin-default-event-tracking-advanced-browser'; ``` ### 2. Instantiate the plugin @@ -44,10 +43,10 @@ import { autoTrackingPlugin } from '@amplitude/plugin-auto-tracking-browser'; The plugin accepts 1 optional parameter, which is an `Object` to configure the allowed tracking options. ```typescript -const plugin = autoTrackingPlugin({ +const plugin = defaultEventTrackingAdvancedPlugin({ cssSelectorAllowlist: [ - '.amp-auto-tracking', - '[amp-auto-tracking]' + '.amp-tracking', + '[amp-tracking]' ], pageUrlAllowlist: [ 'https://amplitude.com', @@ -58,8 +57,8 @@ const plugin = autoTrackingPlugin({ Examples: - The above `cssSelectorAllowlist` will only allow tracking elements like: - - `` - - `Link` + - `` + - `Link` - The above `pageUrlAllowlist` will only allow the elements on URL "https://amplitude.com" or any URL matching the "https://amplitude.com/blog/*" to be tracked #### Options @@ -69,7 +68,7 @@ Examples: |`cssSelectorAllowlist`|`string[]`|`undefined`| When provided, only allow elements matching any selector to be tracked. | |`pageUrlAllowlist`|`(string\|RegExp)[]`|`undefined`| When provided, only allow elements matching URLs to be tracked. | |`shouldTrackEventResolver`|`(actionType: ActionType, element: Element) => boolean`|`undefined`| When provided, overwrite the default filter behavior. | -|`dataAttributePrefix`|`string`|`'data-amp-auto-track-'`| Allow data attributes to be collected in event property. | +|`dataAttributePrefix`|`string`|`'data-amp-track-'`| Allow data attributes to be collected in event property. | ### 3. Install plugin to Amplitude SDK diff --git a/packages/plugin-auto-tracking-browser/jest.config.js b/packages/plugin-default-event-tracking-advanced-browser/jest.config.js similarity index 100% rename from packages/plugin-auto-tracking-browser/jest.config.js rename to packages/plugin-default-event-tracking-advanced-browser/jest.config.js diff --git a/packages/plugin-auto-tracking-browser/package.json b/packages/plugin-default-event-tracking-advanced-browser/package.json similarity index 92% rename from packages/plugin-auto-tracking-browser/package.json rename to packages/plugin-default-event-tracking-advanced-browser/package.json index bb86a8b45..040f13cb9 100644 --- a/packages/plugin-auto-tracking-browser/package.json +++ b/packages/plugin-default-event-tracking-advanced-browser/package.json @@ -1,6 +1,6 @@ { - "name": "@amplitude/plugin-auto-tracking-browser", - "version": "0.2.0", + "name": "@amplitude/plugin-default-event-tracking-advanced-browser", + "version": "0.0.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -10,9 +10,9 @@ "types": "lib/esm/index.d.ts", "sideEffects": false, "publishConfig": { - "tag": "latest" + "access": "public", + "tag": "beta" }, - "private": true, "repository": { "type": "git", "url": "git+https://github.com/amplitude/Amplitude-TypeScript.git" diff --git a/packages/plugin-auto-tracking-browser/rollup.config.js b/packages/plugin-default-event-tracking-advanced-browser/rollup.config.js similarity index 63% rename from packages/plugin-auto-tracking-browser/rollup.config.js rename to packages/plugin-default-event-tracking-advanced-browser/rollup.config.js index 0ea990975..df34d2cc0 100644 --- a/packages/plugin-auto-tracking-browser/rollup.config.js +++ b/packages/plugin-default-event-tracking-advanced-browser/rollup.config.js @@ -1,6 +1,6 @@ import { iife, umd } from '../../scripts/build/rollup.config'; iife.input = umd.input; -iife.output.name = 'amplitudeAutoTrackingPlugin'; +iife.output.name = 'amplitudeDefaultEventTrackingAdvancedPlugin'; export default [umd, iife]; diff --git a/packages/plugin-auto-tracking-browser/src/constants.ts b/packages/plugin-default-event-tracking-advanced-browser/src/constants.ts similarity index 94% rename from packages/plugin-auto-tracking-browser/src/constants.ts rename to packages/plugin-default-event-tracking-advanced-browser/src/constants.ts index 3cb44815d..6adf61369 100644 --- a/packages/plugin-auto-tracking-browser/src/constants.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/constants.ts @@ -1,4 +1,4 @@ -export const PLUGIN_NAME = '@amplitude/plugin-auto-tracking-browser'; +export const PLUGIN_NAME = '@amplitude/plugin-default-event-tracking-advanced-browser'; export const AMPLITUDE_ELEMENT_CLICKED_EVENT = '[Amplitude] Element Clicked'; export const AMPLITUDE_ELEMENT_CHANGED_EVENT = '[Amplitude] Element Changed'; diff --git a/packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts similarity index 98% rename from packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts rename to packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts index 87b1888ef..b3246d6b3 100644 --- a/packages/plugin-auto-tracking-browser/src/auto-tracking-plugin.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts @@ -8,7 +8,7 @@ type BrowserEnrichmentPlugin = EnrichmentPlugin; type ActionType = 'click' | 'change'; const DEFAULT_TAG_ALLOWLIST = ['a', 'button', 'input', 'select', 'textarea', 'label']; -const DEFAULT_DATA_ATTRIBUTE_PREFIX = 'data-amp-auto-track-'; +const DEFAULT_DATA_ATTRIBUTE_PREFIX = 'data-amp-track-'; interface EventListener { element: Element; @@ -48,7 +48,7 @@ interface Options { dataAttributePrefix?: string; } -export const autoTrackingPlugin = (options: Options = {}): BrowserEnrichmentPlugin => { +export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): BrowserEnrichmentPlugin => { const { cssSelectorAllowlist, pageUrlAllowlist, diff --git a/packages/plugin-auto-tracking-browser/src/helpers.ts b/packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts similarity index 100% rename from packages/plugin-auto-tracking-browser/src/helpers.ts rename to packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/index.ts b/packages/plugin-default-event-tracking-advanced-browser/src/index.ts new file mode 100644 index 000000000..7ab523eef --- /dev/null +++ b/packages/plugin-default-event-tracking-advanced-browser/src/index.ts @@ -0,0 +1,4 @@ +export { + defaultEventTrackingAdvancedPlugin as plugin, + defaultEventTrackingAdvancedPlugin, +} from './default-event-tracking-advanced-plugin'; diff --git a/packages/plugin-auto-tracking-browser/src/libs/finder.ts b/packages/plugin-default-event-tracking-advanced-browser/src/libs/finder.ts similarity index 100% rename from packages/plugin-auto-tracking-browser/src/libs/finder.ts rename to packages/plugin-default-event-tracking-advanced-browser/src/libs/finder.ts diff --git a/packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts b/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts similarity index 96% rename from packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts rename to packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts index 4cb765e79..36bb91f80 100644 --- a/packages/plugin-auto-tracking-browser/test/auto-tracking-plugin.test.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts @@ -1,4 +1,4 @@ -import { autoTrackingPlugin } from '../src/auto-tracking-plugin'; +import { defaultEventTrackingAdvancedPlugin } from '../src/default-event-tracking-advanced-plugin'; import { BrowserClient, BrowserConfig, EnrichmentPlugin, Logger } from '@amplitude/analytics-types'; import { createInstance } from '@amplitude/analytics-browser'; @@ -31,7 +31,7 @@ describe('autoTrackingPlugin', () => { pathname: '', search: '', }; - plugin = autoTrackingPlugin(); + plugin = defaultEventTrackingAdvancedPlugin(); }); afterEach(() => { @@ -41,7 +41,7 @@ describe('autoTrackingPlugin', () => { describe('name', () => { test('should return the plugin name', () => { - expect(plugin?.name).toBe('@amplitude/plugin-auto-tracking-browser'); + expect(plugin?.name).toBe('@amplitude/plugin-default-event-tracking-advanced-browser'); }); }); @@ -99,7 +99,7 @@ describe('autoTrackingPlugin', () => { }); }); - describe('auto-tracking events', () => { + describe('auto-tracked events', () => { const API_KEY = 'API_KEY'; const USER_ID = 'USER_ID'; @@ -107,7 +107,7 @@ describe('autoTrackingPlugin', () => { let track: jest.SpyInstance; beforeEach(async () => { - plugin = autoTrackingPlugin(); + plugin = defaultEventTrackingAdvancedPlugin(); instance = createInstance(); await instance.init(API_KEY, USER_ID).promise; track = jest.spyOn(instance, 'track'); @@ -228,7 +228,7 @@ describe('autoTrackingPlugin', () => { div.setAttribute('id', 'my-div-id'); document.body.appendChild(div); - plugin = autoTrackingPlugin(); + plugin = defaultEventTrackingAdvancedPlugin(); const loggerProvider: Partial = { log: jest.fn(), warn: jest.fn(), @@ -256,7 +256,7 @@ describe('autoTrackingPlugin', () => { button.appendChild(buttonText); document.body.appendChild(button); - plugin = autoTrackingPlugin({ cssSelectorAllowlist: ['.my-button-class'] }); + plugin = defaultEventTrackingAdvancedPlugin({ cssSelectorAllowlist: ['.my-button-class'] }); const loggerProvider: Partial = { log: jest.fn(), warn: jest.fn(), @@ -277,7 +277,7 @@ describe('autoTrackingPlugin', () => { }); test('should follow pageUrlAllowlist configuration', async () => { - plugin = autoTrackingPlugin({ pageUrlAllowlist: [new RegExp('https://www.test.com')] }); + plugin = defaultEventTrackingAdvancedPlugin({ pageUrlAllowlist: [new RegExp('https://www.test.com')] }); const loggerProvider: Partial = { log: jest.fn(), warn: jest.fn(), @@ -324,7 +324,7 @@ describe('autoTrackingPlugin', () => { button2.appendChild(buttonText2); document.body.appendChild(button2); - plugin = autoTrackingPlugin({ + plugin = defaultEventTrackingAdvancedPlugin({ shouldTrackEventResolver: (actionType, element) => actionType === 'click' && element.id === 'my-button-id-1' && element.tagName === 'BUTTON', }); @@ -358,7 +358,7 @@ describe('autoTrackingPlugin', () => { button.appendChild(buttonText); document.body.appendChild(button); - plugin = autoTrackingPlugin({ + plugin = defaultEventTrackingAdvancedPlugin({ dataAttributePrefix: 'data-amp-test-', }); const loggerProvider: Partial = { diff --git a/packages/plugin-auto-tracking-browser/test/helpers.test.ts b/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts similarity index 97% rename from packages/plugin-auto-tracking-browser/test/helpers.test.ts rename to packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts index 466edc086..a109ae70a 100644 --- a/packages/plugin-auto-tracking-browser/test/helpers.test.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts @@ -10,7 +10,7 @@ import { getNearestLabel, } from '../src/helpers'; -describe('autoTrackingPlugin helpers', () => { +describe('default-event-tracking-advanced-plugin helpers', () => { describe('isNonSensitiveString', () => { test('should return false when text is missing', () => { const text = null; @@ -177,10 +177,10 @@ describe('autoTrackingPlugin helpers', () => { describe('getAttributesWithPrefix', () => { test('should return attributes when matching the prefix', () => { const element = document.createElement('input'); - element.setAttribute('data-amp-auto-track-hello', 'world'); - element.setAttribute('data-amp-auto-track-time', 'machine'); - element.setAttribute('data-amp-auto-track-test', ''); - const result = getAttributesWithPrefix(element, 'data-amp-auto-track-'); + element.setAttribute('data-amp-track-hello', 'world'); + element.setAttribute('data-amp-track-time', 'machine'); + element.setAttribute('data-amp-track-test', ''); + const result = getAttributesWithPrefix(element, 'data-amp-track-'); expect(result).toEqual({ hello: 'world', time: 'machine', test: '' }); }); @@ -188,7 +188,7 @@ describe('autoTrackingPlugin helpers', () => { const element = document.createElement('input'); element.setAttribute('data-hello', 'world'); element.setAttribute('data-time', 'machine'); - const result = getAttributesWithPrefix(element, 'data-amp-auto-track-'); + const result = getAttributesWithPrefix(element, 'data-amp-track-'); expect(result).toEqual({}); }); diff --git a/packages/plugin-auto-tracking-browser/test/setup.ts b/packages/plugin-default-event-tracking-advanced-browser/test/setup.ts similarity index 100% rename from packages/plugin-auto-tracking-browser/test/setup.ts rename to packages/plugin-default-event-tracking-advanced-browser/test/setup.ts diff --git a/packages/plugin-auto-tracking-browser/tsconfig.es5.json b/packages/plugin-default-event-tracking-advanced-browser/tsconfig.es5.json similarity index 100% rename from packages/plugin-auto-tracking-browser/tsconfig.es5.json rename to packages/plugin-default-event-tracking-advanced-browser/tsconfig.es5.json diff --git a/packages/plugin-auto-tracking-browser/tsconfig.esm.json b/packages/plugin-default-event-tracking-advanced-browser/tsconfig.esm.json similarity index 100% rename from packages/plugin-auto-tracking-browser/tsconfig.esm.json rename to packages/plugin-default-event-tracking-advanced-browser/tsconfig.esm.json diff --git a/packages/plugin-auto-tracking-browser/tsconfig.json b/packages/plugin-default-event-tracking-advanced-browser/tsconfig.json similarity index 100% rename from packages/plugin-auto-tracking-browser/tsconfig.json rename to packages/plugin-default-event-tracking-advanced-browser/tsconfig.json From 1a81f7c2283d9a51a7cfcb472fea94c1c9cf0112 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Wed, 11 Oct 2023 16:48:42 +0000 Subject: [PATCH 181/214] chore(release): publish - @amplitude/plugin-default-event-tracking-advanced-browser@0.0.1 --- .../CHANGELOG.md | 8 ++++++++ .../package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md index e69de29bb..417b2d161 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md +++ b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md @@ -0,0 +1,8 @@ +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## 0.0.1 (2023-10-11) + +**Note:** Version bump only for package @amplitude/plugin-default-event-tracking-advanced-browser diff --git a/packages/plugin-default-event-tracking-advanced-browser/package.json b/packages/plugin-default-event-tracking-advanced-browser/package.json index 040f13cb9..aa103d291 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/package.json +++ b/packages/plugin-default-event-tracking-advanced-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-default-event-tracking-advanced-browser", - "version": "0.0.0", + "version": "0.0.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From e4debe25165311199f69b95a765fb6de3852f219 Mon Sep 17 00:00:00 2001 From: Jesse Wang Date: Wed, 11 Oct 2023 14:06:14 -0700 Subject: [PATCH 182/214] feat(session replay): change non-actionable errors to warnings --- .../package.json | 3 +- .../src/version.ts | 1 - .../session-replay-browser/src/helpers.ts | 5 +- .../src/session-replay.ts | 14 ++-- .../test/session-replay.test.ts | 66 +++++++++---------- 5 files changed, 44 insertions(+), 45 deletions(-) delete mode 100644 packages/plugin-session-replay-browser/src/version.ts diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 4043b7a82..5a322406a 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -31,8 +31,7 @@ "publish": "node ../../scripts/publish/upload-to-s3.js", "test": "jest", "typecheck": "tsc -p ./tsconfig.json", - "version": "yarn add @amplitude/analytics-types@\">=1 <3\" @amplitude/analytics-client-common@\">=1 <3\" @amplitude/analytics-core@\">=1 <3\"", - "version-file": "node -p \"'export const VERSION = \\'' + require('./package.json').version + '\\';'\" > src/version.ts" + "version": "yarn add @amplitude/analytics-types@\">=1 <3\" @amplitude/analytics-client-common@\">=1 <3\" @amplitude/analytics-core@\">=1 <3\"" }, "bugs": { "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" diff --git a/packages/plugin-session-replay-browser/src/version.ts b/packages/plugin-session-replay-browser/src/version.ts deleted file mode 100644 index 4eb5ae9ae..000000000 --- a/packages/plugin-session-replay-browser/src/version.ts +++ /dev/null @@ -1 +0,0 @@ -export const VERSION = '0.6.14'; diff --git a/packages/session-replay-browser/src/helpers.ts b/packages/session-replay-browser/src/helpers.ts index 502aa301a..d4581e788 100644 --- a/packages/session-replay-browser/src/helpers.ts +++ b/packages/session-replay-browser/src/helpers.ts @@ -1,3 +1,4 @@ +import { getGlobalScope } from '@amplitude/analytics-client-common'; import { UNMASK_TEXT_CLASS } from './constants'; export const maskInputFn = (text: string, element: HTMLElement) => { @@ -27,6 +28,6 @@ export const isSessionInSample = function (sessionId: number, sampleRate: number }; export const getCurrentUrl = () => { - // eslint-disable-next-line no-restricted-globals - return typeof window !== 'undefined' ? window.location.href : ''; + const globalScope = getGlobalScope(); + return globalScope?.location ? globalScope.location.href : ''; }; diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index 2954d464f..ff314e035 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -129,7 +129,7 @@ export class SessionReplay implements AmplitudeSessionReplay { this.stopRecordingEvents = null; } catch (error) { const typedError = error as Error; - this.loggerProvider.error(`Error occurred while stopping recording: ${typedError.toString()}`); + this.loggerProvider.warn(`Error occurred while stopping recording: ${typedError.toString()}`); } const sessionIdToSend = sessionId || this.config?.sessionId; if (this.events.length && sessionIdToSend) { @@ -185,7 +185,7 @@ export class SessionReplay implements AmplitudeSessionReplay { getShouldRecord() { if (!this.config) { - this.loggerProvider.warn(`Session is not being recorded due to lack of config, please call sessionReplay.init.`); + this.loggerProvider.error(`Session is not being recorded due to lack of config, please call sessionReplay.init.`); return false; } const globalScope = getGlobalScope(); @@ -271,7 +271,7 @@ export class SessionReplay implements AmplitudeSessionReplay { recordCanvas: false, errorHandler: (error) => { const typedError = error as Error; - this.loggerProvider.error('Error while recording: ', typedError.toString()); + this.loggerProvider.warn('Error while recording: ', typedError.toString()); return true; }, @@ -470,7 +470,7 @@ export class SessionReplay implements AmplitudeSessionReplay { return storedReplaySessionContexts; } catch (e) { - this.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); + this.loggerProvider.warn(`${STORAGE_FAILURE}: ${e as string}`); } return undefined; } @@ -498,7 +498,7 @@ export class SessionReplay implements AmplitudeSessionReplay { }; }); } catch (e) { - this.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); + this.loggerProvider.warn(`${STORAGE_FAILURE}: ${e as string}`); } } @@ -533,14 +533,14 @@ export class SessionReplay implements AmplitudeSessionReplay { return sessionMap; }); } catch (e) { - this.loggerProvider.error(`${STORAGE_FAILURE}: ${e as string}`); + this.loggerProvider.warn(`${STORAGE_FAILURE}: ${e as string}`); } } completeRequest({ context, err, success }: { context: SessionReplayContext; err?: string; success?: string }) { context.sessionId && this.cleanUpSessionEventsStore(context.sessionId, context.sequenceId); if (err) { - this.loggerProvider.error(err); + this.loggerProvider.warn(err); } else if (success) { this.loggerProvider.log(success); } diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index 7f86f2929..ea56eb23a 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -38,7 +38,7 @@ describe('SessionReplayPlugin', () => { const { get, update } = IDBKeyVal as MockedIDBKeyVal; const { record } = RRWeb as MockedRRWeb; let originalFetch: typeof global.fetch; - let location: typeof global.window.location; + let globalSpy: jest.SpyInstance; const mockLoggerProvider: MockedLogger = { error: jest.fn(), log: jest.fn(), @@ -75,13 +75,12 @@ describe('SessionReplayPlugin', () => { status: 200, }), ) as jest.Mock; - jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue(mockGlobalScope); + globalSpy = jest.spyOn(AnalyticsClientCommon, 'getGlobalScope').mockReturnValue(mockGlobalScope); }); afterEach(() => { jest.resetAllMocks(); jest.spyOn(global.Math, 'random').mockRestore(); global.fetch = originalFetch; - global.window.location = location; jest.useRealTimers(); }); describe('init', () => { @@ -642,7 +641,7 @@ describe('SessionReplayPlugin', () => { }); describe('stopRecordingAndSendEvents', () => { - test('it should catch errors', async () => { + test('it should catch errors as warnings', async () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, mockOptions).promise; const mockStopRecordingEvents = jest.fn().mockImplementation(() => { @@ -651,7 +650,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.stopRecordingEvents = mockStopRecordingEvents; sessionReplay.stopRecordingAndSendEvents(); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalled(); + expect(mockLoggerProvider.warn).toHaveBeenCalled(); }); test('it should send events for passed session', async () => { const sessionReplay = new SessionReplay(); @@ -848,7 +847,7 @@ describe('SessionReplayPlugin', () => { const recordArg = record.mock.calls[0][0]; const errorHandlerReturn = recordArg?.errorHandler && recordArg?.errorHandler(new Error('test error')); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalled(); + expect(mockLoggerProvider.warn).toHaveBeenCalled(); expect(errorHandlerReturn).toBe(true); }); }); @@ -1017,11 +1016,11 @@ describe('SessionReplayPlugin', () => { }); describe('getSampleRate', () => { - test('should return undefined if no config set', () => { + test('should return default value if no config set', () => { const sessionReplay = new SessionReplay(); sessionReplay.config = undefined; const sampleRate = sessionReplay.getSampleRate(); - expect(sampleRate).toEqual(0); + expect(sampleRate).toEqual(DEFAULT_SAMPLE_RATE); }); }); @@ -1039,7 +1038,7 @@ describe('SessionReplayPlugin', () => { await sessionReplay.send(context); expect(fetch).not.toHaveBeenCalled(); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalled(); + expect(mockLoggerProvider.warn).toHaveBeenCalled(); }); test('should not send anything if device id not set', async () => { const sessionReplay = new SessionReplay(); @@ -1054,7 +1053,7 @@ describe('SessionReplayPlugin', () => { await sessionReplay.send(context); expect(fetch).not.toHaveBeenCalled(); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalled(); + expect(mockLoggerProvider.warn).toHaveBeenCalled(); }); test('should make a request correctly', async () => { const sessionReplay = new SessionReplay(); @@ -1078,7 +1077,7 @@ describe('SessionReplayPlugin', () => { 'Content-Type': 'application/json', Authorization: 'Bearer static_key', 'X-Client-Sample-Rate': `${DEFAULT_SAMPLE_RATE}`, - 'X-Client-Url': window.location.href, + 'X-Client-Url': '', 'X-Client-Version': VERSION, }, method: 'POST', @@ -1108,7 +1107,7 @@ describe('SessionReplayPlugin', () => { 'Content-Type': 'application/json', Authorization: 'Bearer static_key', 'X-Client-Sample-Rate': `${DEFAULT_SAMPLE_RATE}`, - 'X-Client-Url': window.location.href, + 'X-Client-Url': '', 'X-Client-Version': VERSION, }, method: 'POST', @@ -1268,9 +1267,9 @@ describe('SessionReplayPlugin', () => { await runScheduleTimers(); expect(fetch).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.warn).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual('API Failure'); + expect(mockLoggerProvider.warn.mock.calls[0][0]).toEqual('API Failure'); }); test('should not retry for 400 error', async () => { (fetch as jest.Mock) @@ -1294,7 +1293,7 @@ describe('SessionReplayPlugin', () => { await runScheduleTimers(); expect(fetch).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.warn).toHaveBeenCalledTimes(1); }); test('should not retry for 413 error', async () => { (fetch as jest.Mock) @@ -1315,7 +1314,7 @@ describe('SessionReplayPlugin', () => { await runScheduleTimers(); expect(fetch).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.warn).toHaveBeenCalledTimes(1); }); test('should handle retry for 500 error', async () => { (fetch as jest.Mock) @@ -1386,9 +1385,9 @@ describe('SessionReplayPlugin', () => { await runScheduleTimers(); expect(fetch).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.warn).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual(UNEXPECTED_ERROR_MESSAGE); + expect(mockLoggerProvider.warn.mock.calls[0][0]).toEqual(UNEXPECTED_ERROR_MESSAGE); }); }); @@ -1399,9 +1398,9 @@ describe('SessionReplayPlugin', () => { get.mockImplementationOnce(() => Promise.reject('error')); await sessionReplay.getAllSessionEventsFromStore(); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.warn).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( + expect(mockLoggerProvider.warn.mock.calls[0][0]).toEqual( 'Failed to store session replay events in IndexedDB: error', ); }); @@ -1531,9 +1530,9 @@ describe('SessionReplayPlugin', () => { update.mockImplementationOnce(() => Promise.reject('error')); await sessionReplay.cleanUpSessionEventsStore(123, 1); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.warn).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( + expect(mockLoggerProvider.warn.mock.calls[0][0]).toEqual( 'Failed to store session replay events in IndexedDB: error', ); }); @@ -1690,9 +1689,9 @@ describe('SessionReplayPlugin', () => { await sessionReplay.storeEventsForSession([mockEventString], 0, mockOptions.sessionId as number); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalledTimes(1); + expect(mockLoggerProvider.warn).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockLoggerProvider.error.mock.calls[0][0]).toEqual( + expect(mockLoggerProvider.warn.mock.calls[0][0]).toEqual( 'Failed to store session replay events in IndexedDB: error', ); }); @@ -1797,17 +1796,18 @@ describe('SessionReplayPlugin', () => { }); describe('getCurrentUrl', () => { - let windowSpy: jest.SpyInstance; - beforeEach(() => { - windowSpy = jest.spyOn(globalThis, 'window', 'get'); - }); - - afterEach(() => { - windowSpy.mockRestore(); + test('returns url if exists', () => { + globalSpy.mockImplementation(() => ({ + location: { + href: 'https://www.amplitude.com', + }, + })); + const url = Helpers.getCurrentUrl(); + expect(url).toEqual('https://www.amplitude.com'); }); - test('runs without error', () => { - windowSpy.mockImplementation(() => undefined); + test('returns empty string if url does not exist', () => { + globalSpy.mockImplementation(() => undefined); const url = Helpers.getCurrentUrl(); expect(url).toEqual(''); }); From 33c682d1aee75f01eb341132d97188628076ffe8 Mon Sep 17 00:00:00 2001 From: Jesse Wang <144086244+jxiwang@users.noreply.github.com> Date: Fri, 13 Oct 2023 10:07:13 -0700 Subject: [PATCH 183/214] Update README.md --- packages/session-replay-browser/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/session-replay-browser/README.md b/packages/session-replay-browser/README.md index aaa79dd4d..9eb73d187 100644 --- a/packages/session-replay-browser/README.md +++ b/packages/session-replay-browser/README.md @@ -75,7 +75,7 @@ sessionReplay.shutdown() |-|-|-|-|-| |`deviceId`|`string`|Yes|`undefined`|Sets an identifier for the device running your application.| |`sessionId`|`number`|Yes|`undefined`|Sets an identifier for the users current session. The value must be in milliseconds since epoch (Unix Timestamp).| -|`sampleRate`|`number`|No|`undefined`|Use this option to control how many sessions will be selected for replay collection. A selected session will be collected for replay, while sessions that are not selected will not.

The number should be a decimal between 0 and 1, ie `0.4`, representing the fraction of sessions you would like to have randomly selected for replay collection. Over a large number of sessions, `0.4` would select `40%` of those sessions.| +|`sampleRate`|`number`|No|`0`|Use this option to control how many sessions will be selected for replay collection. A selected session will be collected for replay, while sessions that are not selected will not.

The number should be a decimal between 0 and 1, ie `0.4`, representing the fraction of sessions you would like to have randomly selected for replay collection. Over a large number of sessions, `0.4` would select `40%` of those sessions.| |`optOut`|`boolean`|No|`false`|Sets permission to collect replays for sessions. Setting a value of true prevents Amplitude from collecting session replays.| |`flushMaxRetries`|`number`|No|`5`|Sets the maximum number of retries for failed upload attempts. This is only applicable to retryable errors.| |`logLevel`|`number`|No|`LogLevel.Warn`|`LogLevel.None` or `LogLevel.Error` or `LogLevel.Warn` or `LogLevel.Verbose` or `LogLevel.Debug`. Sets the log level.| From 1a2d6a46bc6600036684fabf262292c04ba7f64b Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Tue, 17 Oct 2023 19:35:58 +0000 Subject: [PATCH 184/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.6.15 - @amplitude/session-replay-browser@0.3.0 --- .../plugin-session-replay-browser/CHANGELOG.md | 9 +++++++++ .../plugin-session-replay-browser/package.json | 4 ++-- packages/session-replay-browser/CHANGELOG.md | 18 ++++++++++++++++++ packages/session-replay-browser/package.json | 2 +- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 2c8d90b34..194576179 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.6.15](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.14...@amplitude/plugin-session-replay-browser@0.6.15) (2023-10-17) + +**Note:** Version bump only for package @amplitude/plugin-session-replay-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.6.14](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.13...@amplitude/plugin-session-replay-browser@0.6.14) (2023-09-27) **Note:** Version bump only for package @amplitude/plugin-session-replay-browser diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 5a322406a..acf8e224a 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.6.14", + "version": "0.6.15", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -40,7 +40,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "^0.2.5", + "@amplitude/session-replay-browser": "^0.3.0", "idb-keyval": "^6.2.1", "rrweb": "^2.0.0-alpha.11", "tslib": "^2.4.1" diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index 4f50fdc3f..d85b5d803 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,24 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.3.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.2.5...@amplitude/session-replay-browser@0.3.0) (2023-10-17) + +### Features + +- **session replay:** add metadata to headers + ([222920a](https://github.com/amplitude/Amplitude-TypeScript/commit/222920a52c34cce5f4b111f9f2768836b248dcdb)) +- **session replay:** add metadata to headers + ([6d15bfb](https://github.com/amplitude/Amplitude-TypeScript/commit/6d15bfb5daccd52d1dc4f7ae1ead8d5f4b2bf6a2)) +- **session replay:** add metadata to headers + ([d4a1b33](https://github.com/amplitude/Amplitude-TypeScript/commit/d4a1b33930bb8d20dd28a10ff9075854348b5272)) +- **session replay:** change non-actionable errors to warnings + ([e4debe2](https://github.com/amplitude/Amplitude-TypeScript/commit/e4debe25165311199f69b95a765fb6de3852f219)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.2.5](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.2.4...@amplitude/session-replay-browser@0.2.5) (2023-09-27) ### Bug Fixes diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index ac1c15ba3..620d44308 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "0.2.5", + "version": "0.3.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 8f96d1ceb7d31dabd2fc8c34aba3d01cfa886f79 Mon Sep 17 00:00:00 2001 From: Kelly Wallach Date: Tue, 24 Oct 2023 12:16:46 -0400 Subject: [PATCH 185/214] fix(session replay): use internal fork of rrweb --- .../package.json | 1 - packages/session-replay-browser/package.json | 2 +- .../src/session-replay.ts | 4 +- .../test/session-replay.test.ts | 6 +- yarn.lock | 66 +++++++++---------- 5 files changed, 39 insertions(+), 40 deletions(-) diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index acf8e224a..623832553 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -42,7 +42,6 @@ "@amplitude/analytics-types": ">=1 <3", "@amplitude/session-replay-browser": "^0.3.0", "idb-keyval": "^6.2.1", - "rrweb": "^2.0.0-alpha.11", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index 620d44308..bf3f7e018 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -41,8 +41,8 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", + "@amplitude/rrweb": "^2.0.0-alpha.12", "idb-keyval": "^6.2.1", - "rrweb": "^2.0.0-alpha.11", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index ff314e035..4e1c079f7 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -1,8 +1,8 @@ import { getAnalyticsConnector, getGlobalScope } from '@amplitude/analytics-client-common'; import { BaseTransport, Logger, returnWrapper } from '@amplitude/analytics-core'; import { Logger as ILogger, ServerZone, Status } from '@amplitude/analytics-types'; +import { pack, record } from '@amplitude/rrweb'; import * as IDBKeyVal from 'idb-keyval'; -import { pack, record } from 'rrweb'; import { SessionReplayConfig } from './config'; import { BLOCK_CLASS, @@ -18,7 +18,7 @@ import { STORAGE_PREFIX, defaultSessionStore, } from './constants'; -import { isSessionInSample, maskInputFn, getCurrentUrl } from './helpers'; +import { getCurrentUrl, isSessionInSample, maskInputFn } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, MISSING_API_KEY_MESSAGE, diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index ea56eb23a..f2f9988ab 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -1,8 +1,8 @@ /* eslint-disable jest/expect-expect */ import * as AnalyticsClientCommon from '@amplitude/analytics-client-common'; import { LogLevel, Logger, ServerZone } from '@amplitude/analytics-types'; +import * as RRWeb from '@amplitude/rrweb'; import * as IDBKeyVal from 'idb-keyval'; -import * as RRWeb from 'rrweb'; import { DEFAULT_SAMPLE_RATE, DEFAULT_SESSION_REPLAY_PROPERTY, SESSION_REPLAY_SERVER_URL } from '../src/constants'; import * as Helpers from '../src/helpers'; import { UNEXPECTED_ERROR_MESSAGE, getSuccessMessage } from '../src/messages'; @@ -13,8 +13,8 @@ import { VERSION } from '../src/version'; jest.mock('idb-keyval'); type MockedIDBKeyVal = jest.Mocked; -jest.mock('rrweb'); -type MockedRRWeb = jest.Mocked; +jest.mock('@amplitude/rrweb'); +type MockedRRWeb = jest.Mocked; type MockedLogger = jest.Mocked; diff --git a/yarn.lock b/yarn.lock index 2a836bfa9..3394581ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -61,6 +61,39 @@ "@amplitude/analytics-types" "^2.1.1" tslib "^2.4.1" +"@amplitude/rrdom@^2.0.0-alpha.12": + version "2.0.0-alpha.12" + resolved "https://registry.yarnpkg.com/@amplitude/rrdom/-/rrdom-2.0.0-alpha.12.tgz#4cbadd8dbe71c21e692a1aff73602d6a956093b1" + integrity sha512-Tfja58hE7pOsuEnISBXkdZ/2pRWqdWLxHCpL431knTr5QGrn4FlSCC6anLtXHfZHyof/dP78lrTEDenRqBxrUA== + dependencies: + "@amplitude/rrweb-snapshot" "^2.0.0-alpha.12" + +"@amplitude/rrweb-snapshot@^2.0.0-alpha.12": + version "2.0.0-alpha.12" + resolved "https://registry.yarnpkg.com/@amplitude/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.12.tgz#70333d55a623a21c84915883dabab0bdaf5dc125" + integrity sha512-YPDSujwf6us5StL58zMN2/l7QvwnLBph9hlZV6yFqYP1CsT3E81N6Dv4R4+1Khyf2f7z+DKmehRCwllE16SLEg== + +"@amplitude/rrweb-types@^2.0.0-alpha.12": + version "2.0.0-alpha.12" + resolved "https://registry.yarnpkg.com/@amplitude/rrweb-types/-/rrweb-types-2.0.0-alpha.12.tgz#af07c4d7d0f9b15b62919aff1370595e4753df20" + integrity sha512-faYhKKhbbWXGNlFcRaN7px4YazXSJLI/lqrCiPggP3FUk60hgrFAYGHSUB2SvX/KE3uxkpzGGGPyTsfE7wg+5g== + dependencies: + "@amplitude/rrweb-snapshot" "^2.0.0-alpha.12" + +"@amplitude/rrweb@^2.0.0-alpha.12": + version "2.0.0-alpha.12" + resolved "https://registry.yarnpkg.com/@amplitude/rrweb/-/rrweb-2.0.0-alpha.12.tgz#5da858976d4300fb237bed494baf367936717a8c" + integrity sha512-XTg1nLVyB2yI5OqLsAdAGa4XbRHqH6u5pGsuM9eNvZRg3jr0O5vLYs75mtrogkwYDBEotitVGhrj0e0v2CqweQ== + dependencies: + "@amplitude/rrdom" "^2.0.0-alpha.12" + "@amplitude/rrweb-snapshot" "^2.0.0-alpha.12" + "@amplitude/rrweb-types" "^2.0.0-alpha.12" + "@types/css-font-loading-module" "0.0.7" + "@xstate/fsm" "^1.4.0" + base64-arraybuffer "^1.0.1" + fflate "^0.4.4" + mitt "^3.0.0" + "@amplitude/ua-parser-js@^0.7.31", "@amplitude/ua-parser-js@^0.7.33": version "0.7.33" resolved "https://registry.yarnpkg.com/@amplitude/ua-parser-js/-/ua-parser-js-0.7.33.tgz#26441a0fb2e956a64e4ede50fb80b848179bb5db" @@ -3832,13 +3865,6 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@rrweb/types@^2.0.0-alpha.11": - version "2.0.0-alpha.11" - resolved "https://registry.yarnpkg.com/@rrweb/types/-/types-2.0.0-alpha.11.tgz#30730ac4b619a698d910f391992be8682286b8c8" - integrity sha512-8ccocIkT5J/bfNRQY85qR/g6p5YQFpgFO2cMt4+Ex7w31Lq0yqZBRaoYEsawQKpLrn5KOHkdn2UTUrna7WMQuA== - dependencies: - rrweb-snapshot "^2.0.0-alpha.11" - "@sideway/address@^4.1.3": version "4.1.4" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" @@ -11076,32 +11102,6 @@ rollup@^2.79.1: optionalDependencies: fsevents "~2.3.2" -rrdom@^2.0.0-alpha.11: - version "2.0.0-alpha.11" - resolved "https://registry.yarnpkg.com/rrdom/-/rrdom-2.0.0-alpha.11.tgz#4f0ca9ecbf22afd317e759762a354350cd4d9986" - integrity sha512-U37m0t4jTz63wnVRcOQ5qFzSTrI5RdNgeXnHAha2Fmh9+1K+XuCx421a8D1wZk3WcDc2sFz/04FVdM0OD2caHg== - dependencies: - rrweb-snapshot "^2.0.0-alpha.11" - -rrweb-snapshot@^2.0.0-alpha.11: - version "2.0.0-alpha.11" - resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.11.tgz#5ce031181ac1ac182ce6706369f77824e2bea17a" - integrity sha512-N0dzeJA2VhrlSOadkKwCVmV/DuNOwBH+Lhx89hAf9PQK4lCS8AP4AaylhqUdZOYHqwVjqsYel/uZ4hN79vuLhw== - -rrweb@^2.0.0-alpha.11: - version "2.0.0-alpha.11" - resolved "https://registry.yarnpkg.com/rrweb/-/rrweb-2.0.0-alpha.11.tgz#1f8a1944e20fd5f3ddc11634a25df2f3832914d7" - integrity sha512-vJ2gNvF+pUG9C2aaau7iSNqhWBSc4BwtUO4FpegOtDObuH4PIaxNJOlgHz82+WxKr9XPm93ER0LqmNpy0KYdKg== - dependencies: - "@rrweb/types" "^2.0.0-alpha.11" - "@types/css-font-loading-module" "0.0.7" - "@xstate/fsm" "^1.4.0" - base64-arraybuffer "^1.0.1" - fflate "^0.4.4" - mitt "^3.0.0" - rrdom "^2.0.0-alpha.11" - rrweb-snapshot "^2.0.0-alpha.11" - run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" From 95f72494ec3c09d596648665838d15bc91018be9 Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Thu, 26 Oct 2023 10:10:36 -0700 Subject: [PATCH 186/214] fix: log error trace in catch blocks (#605) --- packages/analytics-core/src/core-client.ts | 2 +- packages/analytics-core/src/plugins/destination.ts | 2 +- packages/analytics-core/test/plugins/destination.test.ts | 2 +- .../src/ga-events-forwarder.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/analytics-core/src/core-client.ts b/packages/analytics-core/src/core-client.ts index 6d2c84f93..66c739246 100644 --- a/packages/analytics-core/src/core-client.ts +++ b/packages/analytics-core/src/core-client.ts @@ -127,8 +127,8 @@ export class AmplitudeCore implements CoreClient { return result; } catch (e) { + this.config.loggerProvider.error(e); const message = String(e); - this.config.loggerProvider.error(message); const result = buildResult(event, 0, message); return result; diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index 76d74a7fc..df1a3a238 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -166,8 +166,8 @@ export class Destination implements DestinationPlugin { } this.handleResponse(res, list); } catch (e) { + this.config.loggerProvider.error(e); const errorMessage = getErrorMessage(e); - this.config.loggerProvider.error(errorMessage); this.fulfillRequest(list, 0, errorMessage); } } diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index 8b8562d19..0a006e5db 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -1022,7 +1022,7 @@ describe('destination', () => { // eslint-disable-next-line @typescript-eslint/unbound-method expect(loggerProvider.error).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/unbound-method - expect(loggerProvider.error).toHaveBeenCalledWith(message); + expect(loggerProvider.error).toHaveBeenCalledWith(err instanceof Error ? new Error(message) : message); }); test('should parse response without body', async () => { diff --git a/packages/plugin-ga-events-forwarder-browser/src/ga-events-forwarder.ts b/packages/plugin-ga-events-forwarder-browser/src/ga-events-forwarder.ts index 8d385fb13..b2986a000 100644 --- a/packages/plugin-ga-events-forwarder-browser/src/ga-events-forwarder.ts +++ b/packages/plugin-ga-events-forwarder-browser/src/ga-events-forwarder.ts @@ -110,9 +110,9 @@ export const gaEventsForwarderPlugin = ({ measurementIds = [] }: Options = {}): } } return true; - } catch (error) { + } catch (e) { /* istanbul ignore next */ - logger?.error(String(error)); + logger?.error(e); return false; } }; From eb9f8181979aad12e5a1ba1f1afba0e04dc2bc01 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 26 Oct 2023 19:07:43 +0000 Subject: [PATCH 187/214] chore(release): publish - @amplitude/analytics-browser-test@1.4.12 - @amplitude/analytics-browser@1.13.2 - @amplitude/analytics-client-common@1.2.1 - @amplitude/analytics-core@1.2.4 - @amplitude/analytics-node-test@1.0.8 - @amplitude/analytics-node@1.3.4 - @amplitude/analytics-react-native@1.4.5 - @amplitude/marketing-analytics-browser@1.0.12 - @amplitude/plugin-default-event-tracking-advanced-browser@0.0.2 - @amplitude/plugin-ga-events-forwarder-browser@0.3.1 - @amplitude/plugin-page-view-tracking-browser@1.0.11 - @amplitude/plugin-session-replay-browser@0.6.16 - @amplitude/plugin-web-attribution-browser@1.0.11 - @amplitude/session-replay-browser@0.3.1 --- packages/analytics-browser-test/CHANGELOG.md | 9 +++++++++ packages/analytics-browser-test/package.json | 4 ++-- packages/analytics-browser/CHANGELOG.md | 9 +++++++++ packages/analytics-browser/README.md | 2 +- .../analytics-browser/generated/amplitude-snippet.js | 4 ++-- packages/analytics-browser/package.json | 10 +++++----- packages/analytics-browser/playground/amplitude.js | 2 +- packages/analytics-browser/src/version.ts | 2 +- packages/analytics-client-common/CHANGELOG.md | 9 +++++++++ packages/analytics-client-common/package.json | 4 ++-- packages/analytics-core/CHANGELOG.md | 12 ++++++++++++ packages/analytics-core/package.json | 2 +- packages/analytics-node-test/CHANGELOG.md | 9 +++++++++ packages/analytics-node-test/package.json | 4 ++-- packages/analytics-node/CHANGELOG.md | 9 +++++++++ packages/analytics-node/package.json | 4 ++-- packages/analytics-node/src/version.ts | 2 +- packages/analytics-react-native/CHANGELOG.md | 9 +++++++++ packages/analytics-react-native/package.json | 6 +++--- packages/analytics-react-native/src/version.ts | 2 +- packages/marketing-analytics-browser/CHANGELOG.md | 9 +++++++++ packages/marketing-analytics-browser/README.md | 2 +- .../generated/amplitude-gtm-snippet.js | 4 ++-- .../generated/amplitude-snippet.js | 4 ++-- packages/marketing-analytics-browser/package.json | 12 ++++++------ packages/marketing-analytics-browser/src/version.ts | 2 +- .../CHANGELOG.md | 9 +++++++++ .../package.json | 4 ++-- .../plugin-ga-events-forwarder-browser/CHANGELOG.md | 12 ++++++++++++ .../plugin-ga-events-forwarder-browser/package.json | 2 +- .../src/version.ts | 2 +- .../plugin-page-view-tracking-browser/CHANGELOG.md | 9 +++++++++ .../plugin-page-view-tracking-browser/package.json | 4 ++-- packages/plugin-session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/plugin-session-replay-browser/package.json | 4 ++-- packages/plugin-web-attribution-browser/CHANGELOG.md | 9 +++++++++ packages/plugin-web-attribution-browser/package.json | 6 +++--- packages/session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/session-replay-browser/package.json | 2 +- 39 files changed, 186 insertions(+), 48 deletions(-) diff --git a/packages/analytics-browser-test/CHANGELOG.md b/packages/analytics-browser-test/CHANGELOG.md index 55639540d..529505617 100644 --- a/packages/analytics-browser-test/CHANGELOG.md +++ b/packages/analytics-browser-test/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.4.12](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.11...@amplitude/analytics-browser-test@1.4.12) (2023-10-26) + +**Note:** Version bump only for package @amplitude/analytics-browser-test + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.4.11](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser-test@1.4.10...@amplitude/analytics-browser-test@1.4.11) (2023-09-27) **Note:** Version bump only for package @amplitude/analytics-browser-test diff --git a/packages/analytics-browser-test/package.json b/packages/analytics-browser-test/package.json index 8844904a9..cdbda960a 100644 --- a/packages/analytics-browser-test/package.json +++ b/packages/analytics-browser-test/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser-test", - "version": "1.4.11", + "version": "1.4.12", "private": true, "description": "", "author": "Amplitude Inc", @@ -16,7 +16,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.13.1" + "@amplitude/analytics-browser": "^1.13.2" }, "devDependencies": { "nock": "^13.2.4" diff --git a/packages/analytics-browser/CHANGELOG.md b/packages/analytics-browser/CHANGELOG.md index bee607e61..21a71e0d5 100644 --- a/packages/analytics-browser/CHANGELOG.md +++ b/packages/analytics-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.13.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.13.1...@amplitude/analytics-browser@1.13.2) (2023-10-26) + +**Note:** Version bump only for package @amplitude/analytics-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.13.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@1.13.0...@amplitude/analytics-browser@1.13.1) (2023-09-27) ### Bug Fixes diff --git a/packages/analytics-browser/README.md b/packages/analytics-browser/README.md index 888a9cbf8..bb862b4d6 100644 --- a/packages/analytics-browser/README.md +++ b/packages/analytics-browser/README.md @@ -40,7 +40,7 @@ Alternatively, the package is also distributed through a CDN. Copy and paste the ```html diff --git a/packages/analytics-browser/generated/amplitude-snippet.js b/packages/analytics-browser/generated/amplitude-snippet.js index 13541e4f0..d4b37d568 100644 --- a/packages/analytics-browser/generated/amplitude-snippet.js +++ b/packages/analytics-browser/generated/amplitude-snippet.js @@ -56,10 +56,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-uxjIeuRBYErRNi6HlbBflpiMqMt+LcL2LeZfCcStRQhWbSNrCMi0C+nvW8npPWSt'; + as.integrity = 'sha384-+4gu5hljYcYvO7dTm5rpDWCxZD2qVScRqiWEtzl+cjeSURHvbmMoxxKKBtpJGdK9'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.13.1-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/analytics-browser-1.13.2-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/analytics-browser/package.json b/packages/analytics-browser/package.json index e34e84baf..05ae6a115 100644 --- a/packages/analytics-browser/package.json +++ b/packages/analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser", - "version": "1.13.1", + "version": "1.13.2", "description": "Official Amplitude SDK for Web", "keywords": [ "analytics", @@ -44,11 +44,11 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.2.0", - "@amplitude/analytics-core": "^1.2.3", + "@amplitude/analytics-client-common": "^1.2.1", + "@amplitude/analytics-core": "^1.2.4", "@amplitude/analytics-types": "^1.3.3", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.10", - "@amplitude/plugin-web-attribution-browser": "^1.0.10", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.11", + "@amplitude/plugin-web-attribution-browser": "^1.0.11", "@amplitude/ua-parser-js": "^0.7.31", "tslib": "^2.4.1" }, diff --git a/packages/analytics-browser/playground/amplitude.js b/packages/analytics-browser/playground/amplitude.js index c146ddd07..2dbc7a120 100644 --- a/packages/analytics-browser/playground/amplitude.js +++ b/packages/analytics-browser/playground/amplitude.js @@ -1 +1 @@ -!function(){"use strict";var e=function(){function e(){}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:t(),platform:"Web",os:void 0,deviceModel:void 0}},e}(),t=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},i=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),n=function(){return n=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function g(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function b(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!H(t,i))return!1}return!0},H=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=h(t),s=o.next();!s.done;s=o.next()){var a=s.value;if(Array.isArray(a))return!1;if("object"==typeof a)r=r&&K(a);else if(!["number","string"].includes(typeof a))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{s&&!s.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return K(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},W=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return d({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(y.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(y.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(y.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(y.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(y.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(y.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(y.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(y.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(y.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[y.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[y.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===y.ADD?"number"==typeof i:e===y.UNSET||e===y.REMOVE||H(t,i)))},e}(),Z=function(e,t){return d(d({},t),{event_type:w.IDENTIFY,user_properties:e.getUserProperties()})},G=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=E.Unknown),{event:e,code:t,message:i}},J=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return f(this,void 0,void 0,(function(){return v(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){var t;return f(this,void 0,void 0,(function(){var i,n;return v(this,(function(r){switch(r.label){case 0:return i=this.plugins.findIndex((function(t){return t.name===e})),n=this.plugins[i],this.plugins.splice(i,1),[4,null===(t=n.teardown)||void 0===t?void 0:t.call(n)];case 1:return r.sent(),[2]}}))}))},e.prototype.reset=function(e){this.applying=!1,this.plugins.map((function(e){var t;return null===(t=e.teardown)||void 0===t?void 0:t.call(e)})),this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return f(this,void 0,void 0,(function(){var t,i,n,r,o,s,a,u,c,l,p,f,b,y,m,w,_,k,E;return v(this,(function(v){switch(v.label){case 0:if(!e)return[2];t=g(e,1),i=t[0],n=g(e,2),r=n[1],o=this.plugins.filter((function(e){return e.type===I.BEFORE})),v.label=1;case 1:v.trys.push([1,6,7,8]),s=h(o),a=s.next(),v.label=2;case 2:return a.done?[3,5]:[4,a.value.execute(d({},i))];case 3:if(null===(f=v.sent()))return r({event:i,code:0,message:""}),[2];i=f,v.label=4;case 4:return a=s.next(),[3,2];case 5:return[3,8];case 6:return u=v.sent(),w={error:u},[3,8];case 7:try{a&&!a.done&&(_=s.return)&&_.call(s)}finally{if(w)throw w.error}return[7];case 8:c=this.plugins.filter((function(e){return e.type===I.ENRICHMENT})),v.label=9;case 9:v.trys.push([9,14,15,16]),l=h(c),p=l.next(),v.label=10;case 10:return p.done?[3,13]:[4,p.value.execute(d({},i))];case 11:if(null===(f=v.sent()))return r({event:i,code:0,message:""}),[2];i=f,v.label=12;case 12:return p=l.next(),[3,10];case 13:return[3,16];case 14:return b=v.sent(),k={error:b},[3,16];case 15:try{p&&!p.done&&(E=l.return)&&E.call(l)}finally{if(k)throw k.error}return[7];case 16:return y=this.plugins.filter((function(e){return e.type===I.DESTINATION})),m=y.map((function(e){var t=d({},i);return e.execute(t).catch((function(e){return G(t,0,String(e))}))})),Promise.all(m).then((function(e){var t=g(e,1)[0];r(t)})),[2]}}))}))},e.prototype.flush=function(){return f(this,void 0,void 0,(function(){var e,t,i,n=this;return v(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===I.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),Y="Event rejected due to exceeded retry count",X=function(e){return{promise:e||Promise.resolve()}},ee=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new J(this),this.name=e}return e.prototype._init=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return f(this,void 0,void 0,(function(){var t,i,n,r,o,s;return v(this,(function(a){switch(a.label){case 0:t=this[e],this[e]=[],a.label=1;case 1:a.trys.push([1,6,7,8]),i=h(t),n=i.next(),a.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:a.sent(),a.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=a.sent(),o={error:r},[3,8];case 7:try{n&&!n.done&&(s=i.return)&&s.call(i)}finally{if(o)throw o.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,i){var n=function(e,t,i){return d(d(d({},"string"==typeof e?{event_type:e}:e),i),t&&{event_properties:t})}(e,t,i);return X(this.dispatch(n))},e.prototype.identify=function(e,t){var i=Z(e,t);return X(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,i,n){var r=function(e,t,i,n){var r;return d(d({},n),{event_type:w.GROUP_IDENTIFY,group_properties:i.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,i,n);return X(this.dispatch(r))},e.prototype.setGroup=function(e,t,i){var n=function(e,t,i){var n,r=new W;return r.set(e,t),d(d({},i),{event_type:w.IDENTIFY,user_properties:r.getUserProperties(),groups:(n={},n[e]=t,n)})}(e,t,i);return X(this.dispatch(n))},e.prototype.revenue=function(e,t){var i=function(e,t){return d(d({},t),{event_type:w.REVENUE,event_properties:e.getEventProperties()})}(e,t);return X(this.dispatch(i))},e.prototype.add=function(e){return this.config?X(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),X())},e.prototype.remove=function(e){return this.config?X(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),X())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(G(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return f(this,void 0,void 0,(function(){var t=this;return v(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return f(this,void 0,void 0,(function(){var t,i,n;return v(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,G(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),i=String(t),this.config.loggerProvider.error(i),[2,n=G(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return X(this.timeline.flush())},e}(),te=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return K(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?d({},this.properties):{};return e[m.REVENUE_PRODUCT_ID]=this.productId,e[m.REVENUE_QUANTITY]=this.quantity,e[m.REVENUE_PRICE]=this.price,e[m.REVENUE_TYPE]=this.revenueType,e[m.REVENUE]=this.revenue,e},e}(),ie="Amplitude Logger ",ne=function(){function e(){this.logLevel=_.None}return e.prototype.disable=function(){this.logLevel=_.None},e.prototype.enable=function(e){void 0===e&&(e=_.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),f(this,void 0,void 0,(function(){var t,i,n,r=this;return v(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),f(this,void 0,void 0,(function(){var i,n,r,o,s;return v(this,(function(a){switch(a.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,p(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},a.label=1;case 1:return a.trys.push([1,3,,4]),n=ae(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(n,i)];case 2:return null===(r=a.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(r,e),[3,4]):("body"in r?this.fulfillRequest(e,r.statusCode,"".concat(r.status,": ").concat(ue(r))):this.fulfillRequest(e,r.statusCode,r.status),[2]);case 3:return o=a.sent(),s=(u=o)instanceof Error?u.message:String(u),this.config.loggerProvider.error(s),this.fulfillRequest(e,0,s),[3,4];case 4:return[2]}var u}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case E.Success:this.handleSuccessResponse(e,t);break;case E.Invalid:this.handleInvalidResponse(e,t);break;case E.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case E.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=b(b(b(b([],g(Object.values(e.body.eventsWithInvalidFields)),!1),g(Object.values(e.body.eventsWithMissingFields)),!1),g(Object.values(e.body.eventsWithInvalidIdLengths)),!1),g(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(ue(e)),this.addToQueue.apply(this,b([],g(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(ue(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,b([],g(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),a=new Set(r),u=new Set(o),c=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&a.has(t.event.device_id)))return u.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));c.length>0&&this.config.loggerProvider.warn(ue(e)),this.addToQueue.apply(this,b([],g(c),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,b([],g(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(G(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),pe=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},fe=function(e){return function(){var t=d({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},ve=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=h(t.split(".")),o=r.next();!o.done;o=r.next()){var s=o.value;if(!(s in e))return;e=e[s]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},he=function(e,t){return function(){var i,n,r={};try{for(var o=h(t),s=o.next();!s.done;s=o.next()){var a=s.value;r[a]=ve(e,a)}}catch(e){i={error:e}}finally{try{s&&!s.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ge=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,be)},ye=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return f(this,void 0,void 0,(function(){return v(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return f(this,void 0,void 0,(function(){var t;return v(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return f(this,void 0,void 0,(function(){return v(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return f(this,void 0,void 0,(function(){return v(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),me=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,h,g,b,y,m,w,_,I;if("object"!=typeof e)return null;var k=e.code||0,S=this.buildStatus(k);switch(S){case E.Success:return{status:S,statusCode:k,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case E.Invalid:return{status:S,statusCode:k,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case E.PayloadTooLarge:return{status:S,statusCode:k,body:{error:null!==(h=e.error)&&void 0!==h?h:""}};case E.RateLimit:return{status:S,statusCode:k,body:{error:null!==(g=e.error)&&void 0!==g?g:"",epsThreshold:null!==(b=e.eps_threshold)&&void 0!==b?b:0,throttledDevices:null!==(y=e.throttled_devices)&&void 0!==y?y:{},throttledUsers:null!==(m=e.throttled_users)&&void 0!==m?m:{},exceededDailyQuotaDevices:null!==(w=e.exceeded_daily_quota_devices)&&void 0!==w?w:{},exceededDailyQuotaUsers:null!==(_=e.exceeded_daily_quota_users)&&void 0!==_?_:{},throttledEvents:null!==(I=e.throttled_events)&&void 0!==I?I:[]}};case E.Timeout:default:return{status:S,statusCode:k}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?E.Success:429===e?E.RateLimit:413===e?E.PayloadTooLarge:408===e?E.Timeout:e>=400&&e<500?E.Invalid:e>=500?E.Failed:E.Unknown},e}(),we=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[B,t,e.substring(0,i)].filter(Boolean).join("_")},_e=function(){var e,t,i,n;if("undefined"==typeof navigator)return"";var r=navigator.userLanguage;return null!==(n=null!==(i=null!==(t=null===(e=navigator.languages)||void 0===e?void 0:e[0])&&void 0!==t?t:navigator.language)&&void 0!==i?i:r)&&void 0!==n?n:""},Ie=function(){function e(){this.name="identity",this.type=I.BEFORE,this.identityStore=u().identityStore}return e.prototype.execute=function(e){return f(this,void 0,void 0,(function(){var t;return v(this,(function(i){return(t=e.user_properties)&&this.identityStore.editIdentity().updateUserProperties(t).commit(),[2,e]}))}))},e.prototype.setup=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){return e.instanceName&&(this.identityStore=u(e.instanceName).identityStore),[2]}))}))},e}(),ke=function(){function e(e){this.options=d({},e)}return e.prototype.isEnabled=function(){return f(this,void 0,void 0,(function(){var t,i;return v(this,(function(n){switch(n.label){case 0:if(!O())return[2,!1];e.testValue=String(Date.now()),t=new e(this.options),i="AMP_TEST",n.label=1;case 1:return n.trys.push([1,4,5,7]),[4,t.set(i,e.testValue)];case 2:return n.sent(),[4,t.get(i)];case 3:return[2,n.sent()===e.testValue];case 4:return n.sent(),[2,!1];case 5:return[4,t.remove(i)];case 6:return n.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return f(this,void 0,void 0,(function(){var t;return v(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return f(this,void 0,void 0,(function(){var i,n,r;return v(this,(function(o){return i=O(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return f(this,void 0,void 0,(function(){var n,r,o,s,a,u;return v(this,(function(c){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,o=void 0,(r=null!==t?n:-1)&&((s=new Date).setTime(s.getTime()+24*r*60*60*1e3),o=s),a="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),o&&(a+="; expires=".concat(o.toUTCString())),a+="; path=/",this.options.domain&&(a+="; domain=".concat(this.options.domain)),this.options.secure&&(a+="; Secure"),this.options.sameSite&&(a+="; SameSite=".concat(this.options.sameSite)),(u=O())&&(u.document.cookie=a)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return f(this,void 0,void 0,(function(){return v(this,(function(e){return[2]}))}))},e}(),Ee=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return l(t,e),t.prototype.send=function(e,t){return f(this,void 0,void 0,(function(){var i,n;return v(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},t}(me),Se=function(e){var t={};for(var i in e){var n=e[i];n&&(t[i]=n)}return t},Oe=function(){for(var e,t=[],i=0;iDe;try{o=r?JSON.stringify(t.slice(0,De)):JSON.stringify(t),null===(i=O())||void 0===i||i.localStorage.setItem(e,o)}catch(e){}return r&&(s=t.length-De,null===(n=this.loggerProvider)||void 0===n||n.error("Failed to save ".concat(s," events because the queue length exceeded ").concat(De,"."))),[2]}))}))},e.prototype.remove=function(e){var t;return f(this,void 0,void 0,(function(){return v(this,(function(i){try{null===(t=O())||void 0===t||t.localStorage.removeItem(e)}catch(e){}return[2]}))}))},e.prototype.reset=function(){var e;return f(this,void 0,void 0,(function(){return v(this,(function(t){try{null===(e=O())||void 0===e||e.localStorage.clear()}catch(e){}return[2]}))}))},e}(),Ce=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.state={done:4},t}return l(t,e),t.prototype.send=function(e,t){return f(this,void 0,void 0,(function(){var i=this;return v(this,(function(n){return[2,new Promise((function(n,r){"undefined"==typeof XMLHttpRequest&&r(new Error("XHRTransport is not supported."));var o=new XMLHttpRequest;o.open("POST",e,!0),o.onreadystatechange=function(){if(o.readyState===i.state.done)try{var e=o.responseText,t=JSON.parse(e),s=i.buildResponse(t);n(s)}catch(e){r(e)}},o.setRequestHeader("Content-Type","application/json"),o.setRequestHeader("Accept","*/*"),o.send(JSON.stringify(t))}))]}))}))},t}(me),je=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return l(t,e),t.prototype.send=function(e,t){return f(this,void 0,void 0,(function(){var i=this;return v(this,(function(n){return[2,new Promise((function(n,r){var o=O();if(!(null==o?void 0:o.navigator.sendBeacon))throw new Error("SendBeaconTransport is not supported");try{var s=JSON.stringify(t);return n(o.navigator.sendBeacon(e,JSON.stringify(t))?i.buildResponse({code:200,events_ingested:t.events.length,payload_size_bytes:s.length,server_upload_time:Date.now()}):i.buildResponse({code:500}))}catch(e){r(e)}}))]}))}))},t}(me),Le=function(){return{cookieExpiration:365,cookieSameSite:"Lax",cookieSecure:!1,cookieStorage:new ye,cookieUpgrade:!0,disableCookies:!1,domain:"",sessionTimeout:18e5,trackingOptions:{deviceManufacturer:!0,deviceModel:!0,ipAddress:!0,language:!0,osName:!0,osVersion:!0,platform:!0},transportProvider:new Ee}},Me=function(e){function t(t,i){var n,r,o,s,a,u,c,l,p,f=this,v=Le();return(f=e.call(this,d(d({flushIntervalMillis:1e3,flushMaxRetries:5,flushQueueSize:30,transportProvider:v.transportProvider},i),{apiKey:t}))||this)._optOut=!1,f.cookieStorage=null!==(n=null==i?void 0:i.cookieStorage)&&void 0!==n?n:v.cookieStorage,f.deviceId=null==i?void 0:i.deviceId,f.lastEventId=null==i?void 0:i.lastEventId,f.lastEventTime=null==i?void 0:i.lastEventTime,f.optOut=Boolean(null==i?void 0:i.optOut),f.sessionId=null==i?void 0:i.sessionId,f.userId=null==i?void 0:i.userId,f.appVersion=null==i?void 0:i.appVersion,f.attribution=null==i?void 0:i.attribution,f.cookieExpiration=null!==(r=null==i?void 0:i.cookieExpiration)&&void 0!==r?r:v.cookieExpiration,f.cookieSameSite=null!==(o=null==i?void 0:i.cookieSameSite)&&void 0!==o?o:v.cookieSameSite,f.cookieSecure=null!==(s=null==i?void 0:i.cookieSecure)&&void 0!==s?s:v.cookieSecure,f.cookieUpgrade=null!==(a=null==i?void 0:i.cookieUpgrade)&&void 0!==a?a:v.cookieUpgrade,f.defaultTracking=null==i?void 0:i.defaultTracking,f.disableCookies=null!==(u=null==i?void 0:i.disableCookies)&&void 0!==u?u:v.disableCookies,f.defaultTracking=null==i?void 0:i.defaultTracking,f.domain=null!==(c=null==i?void 0:i.domain)&&void 0!==c?c:v.domain,f.partnerId=null==i?void 0:i.partnerId,f.sessionTimeout=null!==(l=null==i?void 0:i.sessionTimeout)&&void 0!==l?l:v.sessionTimeout,f.trackingOptions=null!==(p=null==i?void 0:i.trackingOptions)&&void 0!==p?p:v.trackingOptions,f}return l(t,e),Object.defineProperty(t.prototype,"deviceId",{get:function(){return this._deviceId},set:function(e){this._deviceId!==e&&(this._deviceId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"userId",{get:function(){return this._userId},set:function(e){this._userId!==e&&(this._userId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"sessionId",{get:function(){return this._sessionId},set:function(e){this._sessionId!==e&&(this._sessionId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"optOut",{get:function(){return this._optOut},set:function(e){this._optOut!==e&&(this._optOut=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"lastEventTime",{get:function(){return this._lastEventTime},set:function(e){this._lastEventTime!==e&&(this._lastEventTime=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"lastEventId",{get:function(){return this._lastEventId},set:function(e){this._lastEventId!==e&&(this._lastEventId=e,this.updateStorage())},enumerable:!1,configurable:!0}),t.prototype.updateStorage=function(){var e,t={deviceId:this._deviceId,userId:this._userId,sessionId:this._sessionId,optOut:this._optOut,lastEventTime:this._lastEventTime,lastEventId:this._lastEventId};null===(e=this.cookieStorage)||void 0===e||e.set(we(this.apiKey),t)},t}(oe),Ve=function(e,t){return f(void 0,void 0,void 0,(function(){var i,n,r,o,s,a,u,c,l,p,f,h,g,b,y;return v(this,(function(v){switch(v.label){case 0:return i=Le(),n=null!==(b=null==t?void 0:t.deviceId)&&void 0!==b?b:be(),r=null==t?void 0:t.lastEventId,o=null==t?void 0:t.lastEventTime,s=null==t?void 0:t.optOut,a=null==t?void 0:t.sessionId,u=null==t?void 0:t.userId,c=null==t?void 0:t.cookieStorage,l=null==t?void 0:t.domain,p=Me.bind,f=[void 0,e],h=[d({},t)],g={cookieStorage:c,deviceId:n,domain:l,lastEventId:r,lastEventTime:o,optOut:s,sessionId:a},[4,Be(t)];case 1:return[2,new(p.apply(Me,f.concat([d.apply(void 0,h.concat([(g.storageProvider=v.sent(),g.trackingOptions=d(d({},i.trackingOptions),null==t?void 0:t.trackingOptions),g.transportProvider=null!==(y=null==t?void 0:t.transportProvider)&&void 0!==y?y:$e(null==t?void 0:t.transport),g.userId=u,g)]))])))]}}))}))},ze=function(e,t){return void 0===t&&(t=Le()),f(void 0,void 0,void 0,(function(){var i,n,r;return v(this,(function(o){switch(o.label){case 0:return i=d(d({},t),e),n=null==e?void 0:e.cookieStorage,(r=!n)?[3,2]:[4,n.isEnabled()];case 1:r=!o.sent(),o.label=2;case 2:return r?[2,Fe(i)]:[2,n]}}))}))},Fe=function(e){return f(void 0,void 0,void 0,(function(){var t,i;return v(this,(function(n){switch(n.label){case 0:return t=new ke({domain:e.domain,expirationDays:e.cookieExpiration,sameSite:e.cookieSameSite,secure:e.cookieSecure}),(i=e.disableCookies)?[3,2]:[4,t.isEnabled()];case 1:i=!n.sent(),n.label=2;case 2:return i?[4,(t=new Ae).isEnabled()]:[3,4];case 3:n.sent()||(t=new ye),n.label=4;case 4:return[2,t]}}))}))},Be=function(e){return f(void 0,void 0,void 0,(function(){var t,i,n,r,o,s,a,u,c;return v(this,(function(l){switch(l.label){case 0:if(t=e&&Object.prototype.hasOwnProperty.call(e,"storageProvider"),i=e&&e.loggerProvider,t&&!e.storageProvider)return[3,9];l.label=1;case 1:l.trys.push([1,7,8,9]),n=h([null==e?void 0:e.storageProvider,new Ae({loggerProvider:i})]),r=n.next(),l.label=2;case 2:return r.done?[3,6]:(o=r.value,(s=o)?[4,o.isEnabled()]:[3,4]);case 3:s=l.sent(),l.label=4;case 4:if(s)return[2,o];l.label=5;case 5:return r=n.next(),[3,2];case 6:return[3,9];case 7:return a=l.sent(),u={error:a},[3,9];case 8:try{r&&!r.done&&(c=n.return)&&c.call(n)}finally{if(u)throw u.error}return[7];case 9:return[2,void 0]}}))}))},$e=function(e){return e===S.XHR?new Ce:e===S.SendBeacon?new je:Le().transportProvider},Qe="[Amplitude]",Ke="".concat(Qe," Page Viewed"),He="".concat(Qe," Form Started"),We="".concat(Qe," Form Submitted"),Ze="".concat(Qe," File Downloaded"),Ge="session_start",Je="session_end",Ye="".concat(Qe," File Extension"),Xe="".concat(Qe," File Name"),et="".concat(Qe," Link ID"),tt="".concat(Qe," Link Text"),it="".concat(Qe," Link URL"),nt="".concat(Qe," Form ID"),rt="".concat(Qe," Form Name"),ot="".concat(Qe," Form Destination"),st=function(e,t){return f(void 0,void 0,void 0,(function(){var i,n,r,o,s,a,u,c,l,d,p;return v(this,(function(f){switch(f.label){case 0:return[4,ze(t)];case 1:return i=f.sent(),n=function(e){return"".concat(B.toLowerCase(),"_").concat(e.substring(0,6))}(e),[4,i.getRaw(n)];case 2:return(r=f.sent())?(null!==(p=t.cookieUpgrade)&&void 0!==p?p:Le().cookieUpgrade)?[4,i.remove(n)]:[3,4]:[2,{optOut:!1}];case 3:f.sent(),f.label=4;case 4:return o=g(r.split("."),6),s=o[0],a=o[1],u=o[2],c=o[3],l=o[4],d=o[5],[2,{deviceId:s,userId:ut(a),sessionId:at(c),lastEventId:at(d),lastEventTime:at(l),optOut:Boolean(u)}]}}))}))},at=function(e){var t=parseInt(e,32);if(!isNaN(t))return t},ut=function(e){if(atob&&escape&&e)try{return decodeURIComponent(escape(atob(e)))}catch(e){return}},ct="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},lt={exports:{}};ce=lt,le=lt.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",O="Huawei",T="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",q="Sharp",U="Sony",D="Xiaomi",A="Zebra",C="Facebook",j=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},F=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o350?V(e,350):e,this},this.setUA(f),this};Q.VERSION="0.7.33",Q.BROWSER=j([a,l,"major"]),Q.CPU=j([d]),Q.DEVICE=j([s,c,u,p,f,h,v,g,b]),Q.ENGINE=Q.OS=j([a,l]),ce.exports&&(le=ce.exports=Q),le.UAParser=Q;var K=typeof e!==n&&(e.jQuery||e.Zepto);if(K&&!K.ua){var H=new Q;K.ua=H.getResult(),K.ua.get=function(){return H.getUA()},K.ua.set=function(e){H.setUA(e);var t=H.getResult();for(var i in t)K.ua[i]=t[i]}}}("object"==typeof window?window:ct);var dt=lt.exports,pt=function(){function e(){this.name="context",this.type=I.BEFORE,this.library="amplitude-ts/".concat("1.13.1"),"undefined"!=typeof navigator&&(this.userAgent=navigator.userAgent),this.uaResult=new dt(this.userAgent).getResult()}return e.prototype.setup=function(e){return this.config=e,Promise.resolve(void 0)},e.prototype.execute=function(e){var t,i;return f(this,void 0,void 0,(function(){var n,r,o,s,a,u,c;return v(this,(function(l){return n=(new Date).getTime(),r=this.uaResult.browser.name,o=this.uaResult.browser.version,s=this.uaResult.device.model||this.uaResult.os.name,a=this.uaResult.device.vendor,u=null!==(t=this.config.lastEventId)&&void 0!==t?t:-1,c=null!==(i=e.event_id)&&void 0!==i?i:u+1,this.config.lastEventId=c,e.time||(this.config.lastEventTime=n),[2,d(d(d(d(d(d(d(d(d(d(d(d({user_id:this.config.userId,device_id:this.config.deviceId,session_id:this.config.sessionId,time:n},this.config.appVersion&&{app_version:this.config.appVersion}),this.config.trackingOptions.platform&&{platform:"Web"}),this.config.trackingOptions.osName&&{os_name:r}),this.config.trackingOptions.osVersion&&{os_version:o}),this.config.trackingOptions.deviceManufacturer&&{device_manufacturer:a}),this.config.trackingOptions.deviceModel&&{device_model:s}),this.config.trackingOptions.language&&{language:_e()}),this.config.trackingOptions.ipAddress&&{ip:"$remote"}),{insert_id:be(),partner_id:this.config.partnerId,plan:this.config.plan}),this.config.ingestionMetadata&&{ingestion_metadata:{source_name:this.config.ingestionMetadata.sourceName,source_version:this.config.ingestionMetadata.sourceVersion}}),e),{event_id:c,library:this.library,user_agent:this.userAgent})]}))}))},e}(),ft={page_domain:"".concat(Qe," Page Domain"),page_location:"".concat(Qe," Page Location"),page_path:"".concat(Qe," Page Path"),page_title:"".concat(Qe," Page Title"),page_url:"".concat(Qe," Page URL")},vt=function(){var e,t=[];return{name:"@amplitude/plugin-file-download-tracking-browser",type:I.ENRICHMENT,setup:function(i,n){return f(void 0,void 0,void 0,(function(){var r,o;return v(this,(function(s){return n?("undefined"==typeof document||(r=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=o.exec(i.href),s=null==r?void 0:r[1];s&&function(e,i,n){e.addEventListener(i,n),t.push({element:e,type:i,handler:n})}(e,"click",(function(){var t;s&&n.track(Ze,((t={})[Ye]=s,t[Xe]=i.pathname,t[et]=e.id,t[tt]=e.text,t[it]=e.href,t))}))},o=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(r),"undefined"!=typeof MutationObserver&&(e=new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&r(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(r)}))}))}))).observe(document.body,{subtree:!0,childList:!0})),[2]):(i.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return f(void 0,void 0,void 0,(function(){return v(this,(function(t){return[2,e]}))}))},teardown:function(){return f(void 0,void 0,void 0,(function(){return v(this,(function(i){return null==e||e.disconnect(),t.forEach((function(e){var t=e.element,i=e.type,n=e.handler;null==t||t.removeEventListener(i,n)})),t=[],[2]}))}))}}},ht=function(){var e,t=[],i=function(e,i,n){e.addEventListener(i,n),t.push({element:e,type:i,handler:n})};return{name:"@amplitude/plugin-form-interaction-tracking-browser",type:I.ENRICHMENT,setup:function(t,n){return f(void 0,void 0,void 0,(function(){var r;return v(this,(function(o){return n?("undefined"==typeof document||(r=function(e){var t=!1;i(e,"change",(function(){var i;t||n.track(He,((i={})[nt]=e.id,i[rt]=e.name,i[ot]=e.action,i)),t=!0})),i(e,"submit",(function(){var i,r;t||n.track(He,((i={})[nt]=e.id,i[rt]=e.name,i[ot]=e.action,i)),n.track(We,((r={})[nt]=e.id,r[rt]=e.name,r[ot]=e.action,r)),t=!1}))},Array.from(document.getElementsByTagName("form")).forEach(r),"undefined"!=typeof MutationObserver&&(e=new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&r(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(r)}))}))}))).observe(document.body,{subtree:!0,childList:!0})),[2]):(t.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return f(void 0,void 0,void 0,(function(){return v(this,(function(t){return[2,e]}))}))},teardown:function(){return f(void 0,void 0,void 0,(function(){return v(this,(function(i){return null==e||e.disconnect(),t.forEach((function(e){var t=e.element,i=e.type,n=e.handler;null==t||t.removeEventListener(i,n)})),t=[],[2]}))}))}}},gt=function(e,t){bt(e,t)},bt=function(e,t){for(var i=0;i=0;--r)i.push(t.slice(r).join("."));r=0,a.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(null!==(_=null!==(w=t.sessionId)&&void 0!==w?w:this.config.sessionId)&&void 0!==_?_:Date.now()),$=!0),(Q=u(t.instanceName)).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new de).promise];case 11:return Z.sent(),[4,this.add(new pt).promise];case 12:return Z.sent(),[4,this.add(new Ie).promise];case 13:return Z.sent(),("boolean"==typeof(G=this.config.defaultTracking)?G:null==G?void 0:G.fileDownloads)?[4,this.add(vt()).promise]:[3,15];case 14:Z.sent(),Z.label=15;case 15:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add(ht()).promise]:[3,17];case 16:Z.sent(),Z.label=17;case 17:return(null===(k=this.config.attribution)||void 0===k?void 0:k.disabled)?[3,19]:(K=function(){for(var e,t,i=this,n=[],r=0;rthis.config.sessionTimeout&&this.setSessionId(i),[2,e.prototype.process.call(this,t)]}))}))},t}(ee),wt=function(){var e=new mt;return{init:ge(e.init.bind(e),"init",fe(e),he(e,["config"])),add:ge(e.add.bind(e),"add",fe(e),he(e,["config.apiKey","timeline.plugins"])),remove:ge(e.remove.bind(e),"remove",fe(e),he(e,["config.apiKey","timeline.plugins"])),track:ge(e.track.bind(e),"track",fe(e),he(e,["config.apiKey","timeline.queue.length"])),logEvent:ge(e.logEvent.bind(e),"logEvent",fe(e),he(e,["config.apiKey","timeline.queue.length"])),identify:ge(e.identify.bind(e),"identify",fe(e),he(e,["config.apiKey","timeline.queue.length"])),groupIdentify:ge(e.groupIdentify.bind(e),"groupIdentify",fe(e),he(e,["config.apiKey","timeline.queue.length"])),setGroup:ge(e.setGroup.bind(e),"setGroup",fe(e),he(e,["config.apiKey","timeline.queue.length"])),revenue:ge(e.revenue.bind(e),"revenue",fe(e),he(e,["config.apiKey","timeline.queue.length"])),flush:ge(e.flush.bind(e),"flush",fe(e),he(e,["config.apiKey","timeline.queue.length"])),getUserId:ge(e.getUserId.bind(e),"getUserId",fe(e),he(e,["config","config.userId"])),setUserId:ge(e.setUserId.bind(e),"setUserId",fe(e),he(e,["config","config.userId"])),getDeviceId:ge(e.getDeviceId.bind(e),"getDeviceId",fe(e),he(e,["config","config.deviceId"])),setDeviceId:ge(e.setDeviceId.bind(e),"setDeviceId",fe(e),he(e,["config","config.deviceId"])),reset:ge(e.reset.bind(e),"reset",fe(e),he(e,["config","config.userId","config.deviceId"])),getSessionId:ge(e.getSessionId.bind(e),"getSessionId",fe(e),he(e,["config"])),setSessionId:ge(e.setSessionId.bind(e),"setSessionId",fe(e),he(e,["config"])),extendSession:ge(e.extendSession.bind(e),"extendSession",fe(e),he(e,["config"])),setOptOut:ge(e.setOptOut.bind(e),"setOptOut",fe(e),he(e,["config"])),setTransport:ge(e.setTransport.bind(e),"setTransport",fe(e),he(e,["config"]))}},_t=wt(),It=_t.add,kt=_t.extendSession,Et=_t.flush,St=_t.getDeviceId,Ot=_t.getSessionId,Tt=_t.getUserId,xt=_t.groupIdentify,Pt=_t.identify,Rt=_t.init,Nt=_t.logEvent,qt=_t.remove,Ut=_t.reset,Dt=_t.revenue,At=_t.setDeviceId,Ct=_t.setGroup,jt=_t.setOptOut,Lt=_t.setSessionId,Mt=_t.setTransport,Vt=_t.setUserId,zt=_t.track,Ft=Object.freeze({__proto__:null,add:It,extendSession:kt,flush:Et,getDeviceId:St,getSessionId:Ot,getUserId:Tt,groupIdentify:xt,identify:Pt,init:Rt,logEvent:Nt,remove:qt,reset:Ut,revenue:Dt,setDeviceId:At,setGroup:Ct,setOptOut:jt,setSessionId:Lt,setTransport:Mt,setUserId:Vt,track:zt,Types:F,createInstance:wt,runQueuedFunctions:gt,Revenue:te,Identify:W});!function(){var e=O();if(e){var t=function(e){var t=wt(),i=O();return i&&i.amplitude&&i.amplitude._iq&&e&&(i.amplitude._iq[e]=t),t};if(e.amplitude=Object.assign(e.amplitude||{},Ft,{createInstance:t}),e.amplitude.invoked){var i=e.amplitude._q;e.amplitude._q=[],gt(Ft,i);for(var n=Object.keys(e.amplitude._iq)||[],r=0;r0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),n=function(){return n=Object.assign||function(e){for(var t,i=1,n=arguments.length;i0&&r[r.length-1])||6!==a[0]&&2!==a[0])){s=0;continue}if(3===a[0]&&(!r||a[1]>r[0]&&a[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function g(e,t){var i="function"==typeof Symbol&&e[Symbol.iterator];if(!i)return e;var n,r,o=i.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(n=o.next()).done;)s.push(n.value)}catch(e){r={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(r)throw r.error}}return s}function b(e,t,i){if(i||2===arguments.length)for(var n,r=0,o=t.length;r1e3)return!1;for(var t in e){var i=e[t];if(!H(t,i))return!1}return!0},H=function(e,t){var i,n;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=h(t),s=o.next();!s.done;s=o.next()){var a=s.value;if(Array.isArray(a))return!1;if("object"==typeof a)r=r&&K(a);else if(!["number","string"].includes(typeof a))return!1;if(!r)return!1}}catch(e){i={error:e}}finally{try{s&&!s.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}}else{if(null==t)return!1;if("object"==typeof t)return K(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},W=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return d({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(y.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(y.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(y.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(y.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(y.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(y.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(y.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(y.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(y.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[y.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,i){if(this._validate(e,t,i)){var n=this._properties[e];return void 0===n&&(n={},this._properties[e]=n),n[t]=i,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,i){return void 0===this._properties[y.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===y.ADD?"number"==typeof i:e===y.UNSET||e===y.REMOVE||H(t,i)))},e}(),Z=function(e,t){return d(d({},t),{event_type:w.IDENTIFY,user_properties:e.getUserProperties()})},G=function(e,t,i){return void 0===t&&(t=0),void 0===i&&(i=E.Unknown),{event:e,code:t,message:i}},J=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){return f(this,void 0,void 0,(function(){return v(this,(function(i){switch(i.label){case 0:return[4,e.setup(t,this.client)];case 1:return i.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){var t;return f(this,void 0,void 0,(function(){var i,n;return v(this,(function(r){switch(r.label){case 0:return i=this.plugins.findIndex((function(t){return t.name===e})),n=this.plugins[i],this.plugins.splice(i,1),[4,null===(t=n.teardown)||void 0===t?void 0:t.call(n)];case 1:return r.sent(),[2]}}))}))},e.prototype.reset=function(e){this.applying=!1,this.plugins.map((function(e){var t;return null===(t=e.teardown)||void 0===t?void 0:t.call(e)})),this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(i){t.queue.push([e,i]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return f(this,void 0,void 0,(function(){var t,i,n,r,o,s,a,u,c,l,p,f,b,y,m,w,_,k,E;return v(this,(function(v){switch(v.label){case 0:if(!e)return[2];t=g(e,1),i=t[0],n=g(e,2),r=n[1],o=this.plugins.filter((function(e){return e.type===I.BEFORE})),v.label=1;case 1:v.trys.push([1,6,7,8]),s=h(o),a=s.next(),v.label=2;case 2:return a.done?[3,5]:[4,a.value.execute(d({},i))];case 3:if(null===(f=v.sent()))return r({event:i,code:0,message:""}),[2];i=f,v.label=4;case 4:return a=s.next(),[3,2];case 5:return[3,8];case 6:return u=v.sent(),w={error:u},[3,8];case 7:try{a&&!a.done&&(_=s.return)&&_.call(s)}finally{if(w)throw w.error}return[7];case 8:c=this.plugins.filter((function(e){return e.type===I.ENRICHMENT})),v.label=9;case 9:v.trys.push([9,14,15,16]),l=h(c),p=l.next(),v.label=10;case 10:return p.done?[3,13]:[4,p.value.execute(d({},i))];case 11:if(null===(f=v.sent()))return r({event:i,code:0,message:""}),[2];i=f,v.label=12;case 12:return p=l.next(),[3,10];case 13:return[3,16];case 14:return b=v.sent(),k={error:b},[3,16];case 15:try{p&&!p.done&&(E=l.return)&&E.call(l)}finally{if(k)throw k.error}return[7];case 16:return y=this.plugins.filter((function(e){return e.type===I.DESTINATION})),m=y.map((function(e){var t=d({},i);return e.execute(t).catch((function(e){return G(t,0,String(e))}))})),Promise.all(m).then((function(e){var t=g(e,1)[0];r(t)})),[2]}}))}))},e.prototype.flush=function(){return f(this,void 0,void 0,(function(){var e,t,i,n=this;return v(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return n.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return e.type===I.DESTINATION})),i=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(i)];case 2:return r.sent(),[2]}}))}))},e}(),Y="Event rejected due to exceeded retry count",X=function(e){return{promise:e||Promise.resolve()}},ee=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new J(this),this.name=e}return e.prototype._init=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return f(this,void 0,void 0,(function(){var t,i,n,r,o,s;return v(this,(function(a){switch(a.label){case 0:t=this[e],this[e]=[],a.label=1;case 1:a.trys.push([1,6,7,8]),i=h(t),n=i.next(),a.label=2;case 2:return n.done?[3,5]:[4,(0,n.value)()];case 3:a.sent(),a.label=4;case 4:return n=i.next(),[3,2];case 5:return[3,8];case 6:return r=a.sent(),o={error:r},[3,8];case 7:try{n&&!n.done&&(s=i.return)&&s.call(i)}finally{if(o)throw o.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,i){var n=function(e,t,i){return d(d(d({},"string"==typeof e?{event_type:e}:e),i),t&&{event_properties:t})}(e,t,i);return X(this.dispatch(n))},e.prototype.identify=function(e,t){var i=Z(e,t);return X(this.dispatch(i))},e.prototype.groupIdentify=function(e,t,i,n){var r=function(e,t,i,n){var r;return d(d({},n),{event_type:w.GROUP_IDENTIFY,group_properties:i.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,i,n);return X(this.dispatch(r))},e.prototype.setGroup=function(e,t,i){var n=function(e,t,i){var n,r=new W;return r.set(e,t),d(d({},i),{event_type:w.IDENTIFY,user_properties:r.getUserProperties(),groups:(n={},n[e]=t,n)})}(e,t,i);return X(this.dispatch(n))},e.prototype.revenue=function(e,t){var i=function(e,t){return d(d({},t),{event_type:w.REVENUE,event_properties:e.getEventProperties()})}(e,t);return X(this.dispatch(i))},e.prototype.add=function(e){return this.config?X(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),X())},e.prototype.remove=function(e){return this.config?X(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),X())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(G(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return f(this,void 0,void 0,(function(){var t=this;return v(this,(function(i){return this.config?[2,this.process(e)]:[2,new Promise((function(i){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,i))}))]}))}))},e.prototype.process=function(e){return f(this,void 0,void 0,(function(){var t,i,n;return v(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,G(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(n=r.sent()).code?this.config.loggerProvider.log(n.message):this.config.loggerProvider.error(n.message),[2,n];case 2:return t=r.sent(),this.config.loggerProvider.error(t),i=String(t),[2,n=G(e,0,i)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return X(this.timeline.flush())},e}(),te=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return K(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?d({},this.properties):{};return e[m.REVENUE_PRODUCT_ID]=this.productId,e[m.REVENUE_QUANTITY]=this.quantity,e[m.REVENUE_PRICE]=this.price,e[m.REVENUE_TYPE]=this.revenueType,e[m.REVENUE]=this.revenue,e},e}(),ie="Amplitude Logger ",ne=function(){function e(){this.logLevel=_.None}return e.prototype.disable=function(){this.logLevel=_.None},e.prototype.enable=function(e){void 0===e&&(e=_.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(i.map((function(e){return n.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(i){var n={event:e,attempts:0,callback:function(e){return i(e)},timeout:0};t.addToQueue(n)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],i=0;i0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),f(this,void 0,void 0,(function(){var t,i,n,r=this;return v(this,(function(o){switch(o.label){case 0:return t=[],i=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):i.push(e)})),this.queue=i,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,a=this.config.flushQueueSize,u=Math.max(a,1),n=s.reduce((function(e,t,i){var n=Math.floor(i/u);return e[n]||(e[n]=[]),e[n].push(t),e}),[]),[4,Promise.all(n.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,a,u}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),f(this,void 0,void 0,(function(){var i,n,r,o,s;return v(this,(function(a){switch(a.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];i={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,p(t,["extra"])})),options:{min_id_length:this.config.minIdLength}},a.label=1;case 1:return a.trys.push([1,3,,4]),n=ae(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(n,i)];case 2:return null===(r=a.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(r,e),[3,4]):("body"in r?this.fulfillRequest(e,r.statusCode,"".concat(r.status,": ").concat(ue(r))):this.fulfillRequest(e,r.statusCode,r.status),[2]);case 3:return o=a.sent(),this.config.loggerProvider.error(o),s=(u=o)instanceof Error?u.message:String(u),this.fulfillRequest(e,0,s),[3,4];case 4:return[2]}var u}))}))},e.prototype.handleResponse=function(e,t){var i=e.status;switch(i){case E.Success:this.handleSuccessResponse(e,t);break;case E.Invalid:this.handleInvalidResponse(e,t);break;case E.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case E.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(i,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var i=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var n=b(b(b(b([],g(Object.values(e.body.eventsWithInvalidFields)),!1),g(Object.values(e.body.eventsWithMissingFields)),!1),g(Object.values(e.body.eventsWithInvalidIdLengths)),!1),g(e.body.silencedEvents),!1).flat(),r=new Set(n),o=t.filter((function(t,n){if(!r.has(n))return!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(ue(e)),this.addToQueue.apply(this,b([],g(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(ue(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,b([],g(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var i=this,n=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(n),a=new Set(r),u=new Set(o),c=t.filter((function(t,n){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&a.has(t.event.device_id)))return u.has(n)&&(t.timeout=i.throttleTimeout),!0;i.fulfillRequest([t],e.statusCode,e.body.error)}));c.length>0&&this.config.loggerProvider.warn(ue(e)),this.addToQueue.apply(this,b([],g(c),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,b([],g(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,i){this.saveEvents(),e.forEach((function(e){return e.callback(G(e.event,t,i))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),pe=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},fe=function(e){return function(){var t=d({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},ve=function(e,t){var i,n;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=h(t.split(".")),o=r.next();!o.done;o=r.next()){var s=o.value;if(!(s in e))return;e=e[s]}}catch(e){i={error:e}}finally{try{o&&!o.done&&(n=r.return)&&n.call(r)}finally{if(i)throw i.error}}return e},he=function(e,t){return function(){var i,n,r={};try{for(var o=h(t),s=o.next();!s.done;s=o.next()){var a=s.value;r[a]=ve(e,a)}}catch(e){i={error:e}}finally{try{s&&!s.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return r}},ge=function(e,t,i,n,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,be)},ye=function(){function e(){this.memoryStorage=new Map}return e.prototype.isEnabled=function(){return f(this,void 0,void 0,(function(){return v(this,(function(e){return[2,!0]}))}))},e.prototype.get=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){return[2,this.memoryStorage.get(e)]}))}))},e.prototype.getRaw=function(e){return f(this,void 0,void 0,(function(){var t;return v(this,(function(i){switch(i.label){case 0:return[4,this.get(e)];case 1:return[2,(t=i.sent())?JSON.stringify(t):void 0]}}))}))},e.prototype.set=function(e,t){return f(this,void 0,void 0,(function(){return v(this,(function(i){return this.memoryStorage.set(e,t),[2]}))}))},e.prototype.remove=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){return this.memoryStorage.delete(e),[2]}))}))},e.prototype.reset=function(){return f(this,void 0,void 0,(function(){return v(this,(function(e){return this.memoryStorage.clear(),[2]}))}))},e}(),me=function(){function e(){}return e.prototype.send=function(e,t){return Promise.resolve(null)},e.prototype.buildResponse=function(e){var t,i,n,r,o,s,a,u,c,l,d,p,f,v,h,g,b,y,m,w,_,I;if("object"!=typeof e)return null;var k=e.code||0,S=this.buildStatus(k);switch(S){case E.Success:return{status:S,statusCode:k,body:{eventsIngested:null!==(t=e.events_ingested)&&void 0!==t?t:0,payloadSizeBytes:null!==(i=e.payload_size_bytes)&&void 0!==i?i:0,serverUploadTime:null!==(n=e.server_upload_time)&&void 0!==n?n:0}};case E.Invalid:return{status:S,statusCode:k,body:{error:null!==(r=e.error)&&void 0!==r?r:"",missingField:null!==(o=e.missing_field)&&void 0!==o?o:"",eventsWithInvalidFields:null!==(s=e.events_with_invalid_fields)&&void 0!==s?s:{},eventsWithMissingFields:null!==(a=e.events_with_missing_fields)&&void 0!==a?a:{},eventsWithInvalidIdLengths:null!==(u=e.events_with_invalid_id_lengths)&&void 0!==u?u:{},epsThreshold:null!==(c=e.eps_threshold)&&void 0!==c?c:0,exceededDailyQuotaDevices:null!==(l=e.exceeded_daily_quota_devices)&&void 0!==l?l:{},silencedDevices:null!==(d=e.silenced_devices)&&void 0!==d?d:[],silencedEvents:null!==(p=e.silenced_events)&&void 0!==p?p:[],throttledDevices:null!==(f=e.throttled_devices)&&void 0!==f?f:{},throttledEvents:null!==(v=e.throttled_events)&&void 0!==v?v:[]}};case E.PayloadTooLarge:return{status:S,statusCode:k,body:{error:null!==(h=e.error)&&void 0!==h?h:""}};case E.RateLimit:return{status:S,statusCode:k,body:{error:null!==(g=e.error)&&void 0!==g?g:"",epsThreshold:null!==(b=e.eps_threshold)&&void 0!==b?b:0,throttledDevices:null!==(y=e.throttled_devices)&&void 0!==y?y:{},throttledUsers:null!==(m=e.throttled_users)&&void 0!==m?m:{},exceededDailyQuotaDevices:null!==(w=e.exceeded_daily_quota_devices)&&void 0!==w?w:{},exceededDailyQuotaUsers:null!==(_=e.exceeded_daily_quota_users)&&void 0!==_?_:{},throttledEvents:null!==(I=e.throttled_events)&&void 0!==I?I:[]}};case E.Timeout:default:return{status:S,statusCode:k}}},e.prototype.buildStatus=function(e){return e>=200&&e<300?E.Success:429===e?E.RateLimit:413===e?E.PayloadTooLarge:408===e?E.Timeout:e>=400&&e<500?E.Invalid:e>=500?E.Failed:E.Unknown},e}(),we=function(e,t,i){return void 0===t&&(t=""),void 0===i&&(i=10),[B,t,e.substring(0,i)].filter(Boolean).join("_")},_e=function(){var e,t,i,n;if("undefined"==typeof navigator)return"";var r=navigator.userLanguage;return null!==(n=null!==(i=null!==(t=null===(e=navigator.languages)||void 0===e?void 0:e[0])&&void 0!==t?t:navigator.language)&&void 0!==i?i:r)&&void 0!==n?n:""},Ie=function(){function e(){this.name="identity",this.type=I.BEFORE,this.identityStore=u().identityStore}return e.prototype.execute=function(e){return f(this,void 0,void 0,(function(){var t;return v(this,(function(i){return(t=e.user_properties)&&this.identityStore.editIdentity().updateUserProperties(t).commit(),[2,e]}))}))},e.prototype.setup=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){return e.instanceName&&(this.identityStore=u(e.instanceName).identityStore),[2]}))}))},e}(),ke=function(){function e(e){this.options=d({},e)}return e.prototype.isEnabled=function(){return f(this,void 0,void 0,(function(){var t,i;return v(this,(function(n){switch(n.label){case 0:if(!O())return[2,!1];e.testValue=String(Date.now()),t=new e(this.options),i="AMP_TEST",n.label=1;case 1:return n.trys.push([1,4,5,7]),[4,t.set(i,e.testValue)];case 2:return n.sent(),[4,t.get(i)];case 3:return[2,n.sent()===e.testValue];case 4:return n.sent(),[2,!1];case 5:return[4,t.remove(i)];case 6:return n.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return f(this,void 0,void 0,(function(){var t;return v(this,(function(i){switch(i.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=i.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t;return f(this,void 0,void 0,(function(){var i,n,r;return v(this,(function(o){return i=O(),n=null!==(t=null==i?void 0:i.document.cookie.split("; "))&&void 0!==t?t:[],(r=n.find((function(t){return 0===t.indexOf(e+"=")})))?[2,r.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var i;return f(this,void 0,void 0,(function(){var n,r,o,s,a,u;return v(this,(function(c){try{n=null!==(i=this.options.expirationDays)&&void 0!==i?i:0,o=void 0,(r=null!==t?n:-1)&&((s=new Date).setTime(s.getTime()+24*r*60*60*1e3),o=s),a="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),o&&(a+="; expires=".concat(o.toUTCString())),a+="; path=/",this.options.domain&&(a+="; domain=".concat(this.options.domain)),this.options.secure&&(a+="; Secure"),this.options.sameSite&&(a+="; SameSite=".concat(this.options.sameSite)),(u=O())&&(u.document.cookie=a)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return f(this,void 0,void 0,(function(){return v(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return f(this,void 0,void 0,(function(){return v(this,(function(e){return[2]}))}))},e}(),Ee=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return l(t,e),t.prototype.send=function(e,t){return f(this,void 0,void 0,(function(){var i,n;return v(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return i={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,i)];case 1:return[4,r.sent().json()];case 2:return n=r.sent(),[2,this.buildResponse(n)]}}))}))},t}(me),Se=function(e){var t={};for(var i in e){var n=e[i];n&&(t[i]=n)}return t},Oe=function(){for(var e,t=[],i=0;iDe;try{o=r?JSON.stringify(t.slice(0,De)):JSON.stringify(t),null===(i=O())||void 0===i||i.localStorage.setItem(e,o)}catch(e){}return r&&(s=t.length-De,null===(n=this.loggerProvider)||void 0===n||n.error("Failed to save ".concat(s," events because the queue length exceeded ").concat(De,"."))),[2]}))}))},e.prototype.remove=function(e){var t;return f(this,void 0,void 0,(function(){return v(this,(function(i){try{null===(t=O())||void 0===t||t.localStorage.removeItem(e)}catch(e){}return[2]}))}))},e.prototype.reset=function(){var e;return f(this,void 0,void 0,(function(){return v(this,(function(t){try{null===(e=O())||void 0===e||e.localStorage.clear()}catch(e){}return[2]}))}))},e}(),Ce=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.state={done:4},t}return l(t,e),t.prototype.send=function(e,t){return f(this,void 0,void 0,(function(){var i=this;return v(this,(function(n){return[2,new Promise((function(n,r){"undefined"==typeof XMLHttpRequest&&r(new Error("XHRTransport is not supported."));var o=new XMLHttpRequest;o.open("POST",e,!0),o.onreadystatechange=function(){if(o.readyState===i.state.done)try{var e=o.responseText,t=JSON.parse(e),s=i.buildResponse(t);n(s)}catch(e){r(e)}},o.setRequestHeader("Content-Type","application/json"),o.setRequestHeader("Accept","*/*"),o.send(JSON.stringify(t))}))]}))}))},t}(me),je=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return l(t,e),t.prototype.send=function(e,t){return f(this,void 0,void 0,(function(){var i=this;return v(this,(function(n){return[2,new Promise((function(n,r){var o=O();if(!(null==o?void 0:o.navigator.sendBeacon))throw new Error("SendBeaconTransport is not supported");try{var s=JSON.stringify(t);return n(o.navigator.sendBeacon(e,JSON.stringify(t))?i.buildResponse({code:200,events_ingested:t.events.length,payload_size_bytes:s.length,server_upload_time:Date.now()}):i.buildResponse({code:500}))}catch(e){r(e)}}))]}))}))},t}(me),Le=function(){return{cookieExpiration:365,cookieSameSite:"Lax",cookieSecure:!1,cookieStorage:new ye,cookieUpgrade:!0,disableCookies:!1,domain:"",sessionTimeout:18e5,trackingOptions:{deviceManufacturer:!0,deviceModel:!0,ipAddress:!0,language:!0,osName:!0,osVersion:!0,platform:!0},transportProvider:new Ee}},Me=function(e){function t(t,i){var n,r,o,s,a,u,c,l,p,f=this,v=Le();return(f=e.call(this,d(d({flushIntervalMillis:1e3,flushMaxRetries:5,flushQueueSize:30,transportProvider:v.transportProvider},i),{apiKey:t}))||this)._optOut=!1,f.cookieStorage=null!==(n=null==i?void 0:i.cookieStorage)&&void 0!==n?n:v.cookieStorage,f.deviceId=null==i?void 0:i.deviceId,f.lastEventId=null==i?void 0:i.lastEventId,f.lastEventTime=null==i?void 0:i.lastEventTime,f.optOut=Boolean(null==i?void 0:i.optOut),f.sessionId=null==i?void 0:i.sessionId,f.userId=null==i?void 0:i.userId,f.appVersion=null==i?void 0:i.appVersion,f.attribution=null==i?void 0:i.attribution,f.cookieExpiration=null!==(r=null==i?void 0:i.cookieExpiration)&&void 0!==r?r:v.cookieExpiration,f.cookieSameSite=null!==(o=null==i?void 0:i.cookieSameSite)&&void 0!==o?o:v.cookieSameSite,f.cookieSecure=null!==(s=null==i?void 0:i.cookieSecure)&&void 0!==s?s:v.cookieSecure,f.cookieUpgrade=null!==(a=null==i?void 0:i.cookieUpgrade)&&void 0!==a?a:v.cookieUpgrade,f.defaultTracking=null==i?void 0:i.defaultTracking,f.disableCookies=null!==(u=null==i?void 0:i.disableCookies)&&void 0!==u?u:v.disableCookies,f.defaultTracking=null==i?void 0:i.defaultTracking,f.domain=null!==(c=null==i?void 0:i.domain)&&void 0!==c?c:v.domain,f.partnerId=null==i?void 0:i.partnerId,f.sessionTimeout=null!==(l=null==i?void 0:i.sessionTimeout)&&void 0!==l?l:v.sessionTimeout,f.trackingOptions=null!==(p=null==i?void 0:i.trackingOptions)&&void 0!==p?p:v.trackingOptions,f}return l(t,e),Object.defineProperty(t.prototype,"deviceId",{get:function(){return this._deviceId},set:function(e){this._deviceId!==e&&(this._deviceId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"userId",{get:function(){return this._userId},set:function(e){this._userId!==e&&(this._userId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"sessionId",{get:function(){return this._sessionId},set:function(e){this._sessionId!==e&&(this._sessionId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"optOut",{get:function(){return this._optOut},set:function(e){this._optOut!==e&&(this._optOut=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"lastEventTime",{get:function(){return this._lastEventTime},set:function(e){this._lastEventTime!==e&&(this._lastEventTime=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"lastEventId",{get:function(){return this._lastEventId},set:function(e){this._lastEventId!==e&&(this._lastEventId=e,this.updateStorage())},enumerable:!1,configurable:!0}),t.prototype.updateStorage=function(){var e,t={deviceId:this._deviceId,userId:this._userId,sessionId:this._sessionId,optOut:this._optOut,lastEventTime:this._lastEventTime,lastEventId:this._lastEventId};null===(e=this.cookieStorage)||void 0===e||e.set(we(this.apiKey),t)},t}(oe),Ve=function(e,t){return f(void 0,void 0,void 0,(function(){var i,n,r,o,s,a,u,c,l,p,f,h,g,b,y;return v(this,(function(v){switch(v.label){case 0:return i=Le(),n=null!==(b=null==t?void 0:t.deviceId)&&void 0!==b?b:be(),r=null==t?void 0:t.lastEventId,o=null==t?void 0:t.lastEventTime,s=null==t?void 0:t.optOut,a=null==t?void 0:t.sessionId,u=null==t?void 0:t.userId,c=null==t?void 0:t.cookieStorage,l=null==t?void 0:t.domain,p=Me.bind,f=[void 0,e],h=[d({},t)],g={cookieStorage:c,deviceId:n,domain:l,lastEventId:r,lastEventTime:o,optOut:s,sessionId:a},[4,Be(t)];case 1:return[2,new(p.apply(Me,f.concat([d.apply(void 0,h.concat([(g.storageProvider=v.sent(),g.trackingOptions=d(d({},i.trackingOptions),null==t?void 0:t.trackingOptions),g.transportProvider=null!==(y=null==t?void 0:t.transportProvider)&&void 0!==y?y:$e(null==t?void 0:t.transport),g.userId=u,g)]))])))]}}))}))},ze=function(e,t){return void 0===t&&(t=Le()),f(void 0,void 0,void 0,(function(){var i,n,r;return v(this,(function(o){switch(o.label){case 0:return i=d(d({},t),e),n=null==e?void 0:e.cookieStorage,(r=!n)?[3,2]:[4,n.isEnabled()];case 1:r=!o.sent(),o.label=2;case 2:return r?[2,Fe(i)]:[2,n]}}))}))},Fe=function(e){return f(void 0,void 0,void 0,(function(){var t,i;return v(this,(function(n){switch(n.label){case 0:return t=new ke({domain:e.domain,expirationDays:e.cookieExpiration,sameSite:e.cookieSameSite,secure:e.cookieSecure}),(i=e.disableCookies)?[3,2]:[4,t.isEnabled()];case 1:i=!n.sent(),n.label=2;case 2:return i?[4,(t=new Ae).isEnabled()]:[3,4];case 3:n.sent()||(t=new ye),n.label=4;case 4:return[2,t]}}))}))},Be=function(e){return f(void 0,void 0,void 0,(function(){var t,i,n,r,o,s,a,u,c;return v(this,(function(l){switch(l.label){case 0:if(t=e&&Object.prototype.hasOwnProperty.call(e,"storageProvider"),i=e&&e.loggerProvider,t&&!e.storageProvider)return[3,9];l.label=1;case 1:l.trys.push([1,7,8,9]),n=h([null==e?void 0:e.storageProvider,new Ae({loggerProvider:i})]),r=n.next(),l.label=2;case 2:return r.done?[3,6]:(o=r.value,(s=o)?[4,o.isEnabled()]:[3,4]);case 3:s=l.sent(),l.label=4;case 4:if(s)return[2,o];l.label=5;case 5:return r=n.next(),[3,2];case 6:return[3,9];case 7:return a=l.sent(),u={error:a},[3,9];case 8:try{r&&!r.done&&(c=n.return)&&c.call(n)}finally{if(u)throw u.error}return[7];case 9:return[2,void 0]}}))}))},$e=function(e){return e===S.XHR?new Ce:e===S.SendBeacon?new je:Le().transportProvider},Qe="[Amplitude]",Ke="".concat(Qe," Page Viewed"),He="".concat(Qe," Form Started"),We="".concat(Qe," Form Submitted"),Ze="".concat(Qe," File Downloaded"),Ge="session_start",Je="session_end",Ye="".concat(Qe," File Extension"),Xe="".concat(Qe," File Name"),et="".concat(Qe," Link ID"),tt="".concat(Qe," Link Text"),it="".concat(Qe," Link URL"),nt="".concat(Qe," Form ID"),rt="".concat(Qe," Form Name"),ot="".concat(Qe," Form Destination"),st=function(e,t){return f(void 0,void 0,void 0,(function(){var i,n,r,o,s,a,u,c,l,d,p;return v(this,(function(f){switch(f.label){case 0:return[4,ze(t)];case 1:return i=f.sent(),n=function(e){return"".concat(B.toLowerCase(),"_").concat(e.substring(0,6))}(e),[4,i.getRaw(n)];case 2:return(r=f.sent())?(null!==(p=t.cookieUpgrade)&&void 0!==p?p:Le().cookieUpgrade)?[4,i.remove(n)]:[3,4]:[2,{optOut:!1}];case 3:f.sent(),f.label=4;case 4:return o=g(r.split("."),6),s=o[0],a=o[1],u=o[2],c=o[3],l=o[4],d=o[5],[2,{deviceId:s,userId:ut(a),sessionId:at(c),lastEventId:at(d),lastEventTime:at(l),optOut:Boolean(u)}]}}))}))},at=function(e){var t=parseInt(e,32);if(!isNaN(t))return t},ut=function(e){if(atob&&escape&&e)try{return decodeURIComponent(escape(atob(e)))}catch(e){return}},ct="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},lt={exports:{}};ce=lt,le=lt.exports,function(e,t){var i="function",n="undefined",r="object",o="string",s="model",a="name",u="type",c="vendor",l="version",d="architecture",p="console",f="mobile",v="tablet",h="smarttv",g="wearable",b="embedded",y="Amazon",m="Apple",w="ASUS",_="BlackBerry",I="Browser",k="Chrome",E="Firefox",S="Google",O="Huawei",T="LG",x="Microsoft",P="Motorola",R="Opera",N="Samsung",q="Sharp",U="Sony",D="Xiaomi",A="Zebra",C="Facebook",j=function(e){for(var t={},i=0;i0?2===u.length?typeof u[1]==i?this[u[0]]=u[1].call(this,l):this[u[0]]=u[1]:3===u.length?typeof u[1]!==i||u[1].exec&&u[1].test?this[u[0]]=l?l.replace(u[1],u[2]):t:this[u[0]]=l?u[1].call(this,l,u[2]):t:4===u.length&&(this[u[0]]=l?u[3].call(this,l.replace(u[1],u[2])):t):this[u]=l||t;d+=2}},F=function(e,i){for(var n in i)if(typeof i[n]===r&&i[n].length>0){for(var o=0;o350?V(e,350):e,this},this.setUA(f),this};Q.VERSION="0.7.33",Q.BROWSER=j([a,l,"major"]),Q.CPU=j([d]),Q.DEVICE=j([s,c,u,p,f,h,v,g,b]),Q.ENGINE=Q.OS=j([a,l]),ce.exports&&(le=ce.exports=Q),le.UAParser=Q;var K=typeof e!==n&&(e.jQuery||e.Zepto);if(K&&!K.ua){var H=new Q;K.ua=H.getResult(),K.ua.get=function(){return H.getUA()},K.ua.set=function(e){H.setUA(e);var t=H.getResult();for(var i in t)K.ua[i]=t[i]}}}("object"==typeof window?window:ct);var dt=lt.exports,pt=function(){function e(){this.name="context",this.type=I.BEFORE,this.library="amplitude-ts/".concat("1.13.2"),"undefined"!=typeof navigator&&(this.userAgent=navigator.userAgent),this.uaResult=new dt(this.userAgent).getResult()}return e.prototype.setup=function(e){return this.config=e,Promise.resolve(void 0)},e.prototype.execute=function(e){var t,i;return f(this,void 0,void 0,(function(){var n,r,o,s,a,u,c;return v(this,(function(l){return n=(new Date).getTime(),r=this.uaResult.browser.name,o=this.uaResult.browser.version,s=this.uaResult.device.model||this.uaResult.os.name,a=this.uaResult.device.vendor,u=null!==(t=this.config.lastEventId)&&void 0!==t?t:-1,c=null!==(i=e.event_id)&&void 0!==i?i:u+1,this.config.lastEventId=c,e.time||(this.config.lastEventTime=n),[2,d(d(d(d(d(d(d(d(d(d(d(d({user_id:this.config.userId,device_id:this.config.deviceId,session_id:this.config.sessionId,time:n},this.config.appVersion&&{app_version:this.config.appVersion}),this.config.trackingOptions.platform&&{platform:"Web"}),this.config.trackingOptions.osName&&{os_name:r}),this.config.trackingOptions.osVersion&&{os_version:o}),this.config.trackingOptions.deviceManufacturer&&{device_manufacturer:a}),this.config.trackingOptions.deviceModel&&{device_model:s}),this.config.trackingOptions.language&&{language:_e()}),this.config.trackingOptions.ipAddress&&{ip:"$remote"}),{insert_id:be(),partner_id:this.config.partnerId,plan:this.config.plan}),this.config.ingestionMetadata&&{ingestion_metadata:{source_name:this.config.ingestionMetadata.sourceName,source_version:this.config.ingestionMetadata.sourceVersion}}),e),{event_id:c,library:this.library,user_agent:this.userAgent})]}))}))},e}(),ft={page_domain:"".concat(Qe," Page Domain"),page_location:"".concat(Qe," Page Location"),page_path:"".concat(Qe," Page Path"),page_title:"".concat(Qe," Page Title"),page_url:"".concat(Qe," Page URL")},vt=function(){var e,t=[];return{name:"@amplitude/plugin-file-download-tracking-browser",type:I.ENRICHMENT,setup:function(i,n){return f(void 0,void 0,void 0,(function(){var r,o;return v(this,(function(s){return n?("undefined"==typeof document||(r=function(e){var i;try{i=new URL(e.href,window.location.href)}catch(e){return}var r=o.exec(i.href),s=null==r?void 0:r[1];s&&function(e,i,n){e.addEventListener(i,n),t.push({element:e,type:i,handler:n})}(e,"click",(function(){var t;s&&n.track(Ze,((t={})[Ye]=s,t[Xe]=i.pathname,t[et]=e.id,t[tt]=e.text,t[it]=e.href,t))}))},o=/\.(pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma)$/,Array.from(document.getElementsByTagName("a")).forEach(r),"undefined"!=typeof MutationObserver&&(e=new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"A"===e.nodeName&&r(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("a")).map(r)}))}))}))).observe(document.body,{subtree:!0,childList:!0})),[2]):(i.loggerProvider.warn("File download tracking requires a later version of @amplitude/analytics-browser. File download events are not tracked."),[2])}))}))},execute:function(e){return f(void 0,void 0,void 0,(function(){return v(this,(function(t){return[2,e]}))}))},teardown:function(){return f(void 0,void 0,void 0,(function(){return v(this,(function(i){return null==e||e.disconnect(),t.forEach((function(e){var t=e.element,i=e.type,n=e.handler;null==t||t.removeEventListener(i,n)})),t=[],[2]}))}))}}},ht=function(){var e,t=[],i=function(e,i,n){e.addEventListener(i,n),t.push({element:e,type:i,handler:n})};return{name:"@amplitude/plugin-form-interaction-tracking-browser",type:I.ENRICHMENT,setup:function(t,n){return f(void 0,void 0,void 0,(function(){var r;return v(this,(function(o){return n?("undefined"==typeof document||(r=function(e){var t=!1;i(e,"change",(function(){var i;t||n.track(He,((i={})[nt]=e.id,i[rt]=e.name,i[ot]=e.action,i)),t=!0})),i(e,"submit",(function(){var i,r;t||n.track(He,((i={})[nt]=e.id,i[rt]=e.name,i[ot]=e.action,i)),n.track(We,((r={})[nt]=e.id,r[rt]=e.name,r[ot]=e.action,r)),t=!1}))},Array.from(document.getElementsByTagName("form")).forEach(r),"undefined"!=typeof MutationObserver&&(e=new MutationObserver((function(e){e.forEach((function(e){e.addedNodes.forEach((function(e){"FORM"===e.nodeName&&r(e),"querySelectorAll"in e&&"function"==typeof e.querySelectorAll&&Array.from(e.querySelectorAll("form")).map(r)}))}))}))).observe(document.body,{subtree:!0,childList:!0})),[2]):(t.loggerProvider.warn("Form interaction tracking requires a later version of @amplitude/analytics-browser. Form interaction events are not tracked."),[2])}))}))},execute:function(e){return f(void 0,void 0,void 0,(function(){return v(this,(function(t){return[2,e]}))}))},teardown:function(){return f(void 0,void 0,void 0,(function(){return v(this,(function(i){return null==e||e.disconnect(),t.forEach((function(e){var t=e.element,i=e.type,n=e.handler;null==t||t.removeEventListener(i,n)})),t=[],[2]}))}))}}},gt=function(e,t){bt(e,t)},bt=function(e,t){for(var i=0;i=0;--r)i.push(t.slice(r).join("."));r=0,a.label=2;case 2:return rthis.config.sessionTimeout)&&(this.setSessionId(null!==(_=null!==(w=t.sessionId)&&void 0!==w?w:this.config.sessionId)&&void 0!==_?_:Date.now()),$=!0),(Q=u(t.instanceName)).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new de).promise];case 11:return Z.sent(),[4,this.add(new pt).promise];case 12:return Z.sent(),[4,this.add(new Ie).promise];case 13:return Z.sent(),("boolean"==typeof(G=this.config.defaultTracking)?G:null==G?void 0:G.fileDownloads)?[4,this.add(vt()).promise]:[3,15];case 14:Z.sent(),Z.label=15;case 15:return function(e){return"boolean"==typeof e?e:!!(null==e?void 0:e.formInteractions)}(this.config.defaultTracking)?[4,this.add(ht()).promise]:[3,17];case 16:Z.sent(),Z.label=17;case 17:return(null===(k=this.config.attribution)||void 0===k?void 0:k.disabled)?[3,19]:(K=function(){for(var e,t,i=this,n=[],r=0;rthis.config.sessionTimeout&&this.setSessionId(i),[2,e.prototype.process.call(this,t)]}))}))},t}(ee),wt=function(){var e=new mt;return{init:ge(e.init.bind(e),"init",fe(e),he(e,["config"])),add:ge(e.add.bind(e),"add",fe(e),he(e,["config.apiKey","timeline.plugins"])),remove:ge(e.remove.bind(e),"remove",fe(e),he(e,["config.apiKey","timeline.plugins"])),track:ge(e.track.bind(e),"track",fe(e),he(e,["config.apiKey","timeline.queue.length"])),logEvent:ge(e.logEvent.bind(e),"logEvent",fe(e),he(e,["config.apiKey","timeline.queue.length"])),identify:ge(e.identify.bind(e),"identify",fe(e),he(e,["config.apiKey","timeline.queue.length"])),groupIdentify:ge(e.groupIdentify.bind(e),"groupIdentify",fe(e),he(e,["config.apiKey","timeline.queue.length"])),setGroup:ge(e.setGroup.bind(e),"setGroup",fe(e),he(e,["config.apiKey","timeline.queue.length"])),revenue:ge(e.revenue.bind(e),"revenue",fe(e),he(e,["config.apiKey","timeline.queue.length"])),flush:ge(e.flush.bind(e),"flush",fe(e),he(e,["config.apiKey","timeline.queue.length"])),getUserId:ge(e.getUserId.bind(e),"getUserId",fe(e),he(e,["config","config.userId"])),setUserId:ge(e.setUserId.bind(e),"setUserId",fe(e),he(e,["config","config.userId"])),getDeviceId:ge(e.getDeviceId.bind(e),"getDeviceId",fe(e),he(e,["config","config.deviceId"])),setDeviceId:ge(e.setDeviceId.bind(e),"setDeviceId",fe(e),he(e,["config","config.deviceId"])),reset:ge(e.reset.bind(e),"reset",fe(e),he(e,["config","config.userId","config.deviceId"])),getSessionId:ge(e.getSessionId.bind(e),"getSessionId",fe(e),he(e,["config"])),setSessionId:ge(e.setSessionId.bind(e),"setSessionId",fe(e),he(e,["config"])),extendSession:ge(e.extendSession.bind(e),"extendSession",fe(e),he(e,["config"])),setOptOut:ge(e.setOptOut.bind(e),"setOptOut",fe(e),he(e,["config"])),setTransport:ge(e.setTransport.bind(e),"setTransport",fe(e),he(e,["config"]))}},_t=wt(),It=_t.add,kt=_t.extendSession,Et=_t.flush,St=_t.getDeviceId,Ot=_t.getSessionId,Tt=_t.getUserId,xt=_t.groupIdentify,Pt=_t.identify,Rt=_t.init,Nt=_t.logEvent,qt=_t.remove,Ut=_t.reset,Dt=_t.revenue,At=_t.setDeviceId,Ct=_t.setGroup,jt=_t.setOptOut,Lt=_t.setSessionId,Mt=_t.setTransport,Vt=_t.setUserId,zt=_t.track,Ft=Object.freeze({__proto__:null,add:It,extendSession:kt,flush:Et,getDeviceId:St,getSessionId:Ot,getUserId:Tt,groupIdentify:xt,identify:Pt,init:Rt,logEvent:Nt,remove:qt,reset:Ut,revenue:Dt,setDeviceId:At,setGroup:Ct,setOptOut:jt,setSessionId:Lt,setTransport:Mt,setUserId:Vt,track:zt,Types:F,createInstance:wt,runQueuedFunctions:gt,Revenue:te,Identify:W});!function(){var e=O();if(e){var t=function(e){var t=wt(),i=O();return i&&i.amplitude&&i.amplitude._iq&&e&&(i.amplitude._iq[e]=t),t};if(e.amplitude=Object.assign(e.amplitude||{},Ft,{createInstance:t}),e.amplitude.invoked){var i=e.amplitude._q;e.amplitude._q=[],gt(Ft,i);for(var n=Object.keys(e.amplitude._iq)||[],r=0;r ```html diff --git a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js index 91e30fbbc..133ab42f8 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-gtm-snippet.js @@ -56,10 +56,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-rE63ZeLIe1QOC1oQn9rIioeynmfPgHUvl7CoUwQpZe3odr3MLSAcmrlOcbhvZrMl'; + as.integrity = 'sha384-KlIoSLjn61hwineeoN6srzf7RWg3hC81YlRphFPY/VKOi8rQ4xmdeVj9JXLp0rJO'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.11-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-gtm-1.0.12-min.js.gz'; as.onload = function () { if (!window.amplitudeGTM.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/generated/amplitude-snippet.js b/packages/marketing-analytics-browser/generated/amplitude-snippet.js index a7c8aae1a..ebff57d13 100644 --- a/packages/marketing-analytics-browser/generated/amplitude-snippet.js +++ b/packages/marketing-analytics-browser/generated/amplitude-snippet.js @@ -56,10 +56,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-sBSss0pKJEOvJthpmyRpY+A6ITpyXzAu5NsLzKC+IBw7JHJwlo442Afg8SWa4KOU'; + as.integrity = 'sha384-/KWIEF35XLQIbC5uph0gkNNfNtc0W84SF53ALp1OZ9cpC0TogMP8F0ViTNBuhELL'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.11-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/marketing-analytics-browser-1.0.12-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/marketing-analytics-browser/package.json b/packages/marketing-analytics-browser/package.json index f0766f8e2..7a72b2505 100644 --- a/packages/marketing-analytics-browser/package.json +++ b/packages/marketing-analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/marketing-analytics-browser", - "version": "1.0.11", + "version": "1.0.12", "description": "Official Amplitude SDK for Web and Marketing Analytics", "keywords": [ "analytics", @@ -43,12 +43,12 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-browser": "^1.13.1", - "@amplitude/analytics-client-common": "^1.2.0", - "@amplitude/analytics-core": "^1.2.3", + "@amplitude/analytics-browser": "^1.13.2", + "@amplitude/analytics-client-common": "^1.2.1", + "@amplitude/analytics-core": "^1.2.4", "@amplitude/analytics-types": "^1.3.3", - "@amplitude/plugin-page-view-tracking-browser": "^1.0.10", - "@amplitude/plugin-web-attribution-browser": "^1.0.10", + "@amplitude/plugin-page-view-tracking-browser": "^1.0.11", + "@amplitude/plugin-web-attribution-browser": "^1.0.11", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/marketing-analytics-browser/src/version.ts b/packages/marketing-analytics-browser/src/version.ts index 8b9f2796e..d13771507 100644 --- a/packages/marketing-analytics-browser/src/version.ts +++ b/packages/marketing-analytics-browser/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.0.11'; +export const VERSION = '1.0.12'; diff --git a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md index 417b2d161..ca1542daf 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md +++ b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.0.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.0.1...@amplitude/plugin-default-event-tracking-advanced-browser@0.0.2) (2023-10-26) + +**Note:** Version bump only for package @amplitude/plugin-default-event-tracking-advanced-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## 0.0.1 (2023-10-11) **Note:** Version bump only for package @amplitude/plugin-default-event-tracking-advanced-browser diff --git a/packages/plugin-default-event-tracking-advanced-browser/package.json b/packages/plugin-default-event-tracking-advanced-browser/package.json index aa103d291..15f95800b 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/package.json +++ b/packages/plugin-default-event-tracking-advanced-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-default-event-tracking-advanced-browser", - "version": "0.0.1", + "version": "0.0.2", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -37,7 +37,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": ">=1 <3", + "@amplitude/analytics-client-common": "^1.2.1", "@amplitude/analytics-types": ">=1 <3", "tslib": "^2.4.1" }, diff --git a/packages/plugin-ga-events-forwarder-browser/CHANGELOG.md b/packages/plugin-ga-events-forwarder-browser/CHANGELOG.md index d5ac38bbe..7c33aba45 100644 --- a/packages/plugin-ga-events-forwarder-browser/CHANGELOG.md +++ b/packages/plugin-ga-events-forwarder-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.3.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-ga-events-forwarder-browser@0.3.0...@amplitude/plugin-ga-events-forwarder-browser@0.3.1) (2023-10-26) + +### Bug Fixes + +- log error trace in catch blocks ([#605](https://github.com/amplitude/Amplitude-TypeScript/issues/605)) + ([95f7249](https://github.com/amplitude/Amplitude-TypeScript/commit/95f72494ec3c09d596648665838d15bc91018be9)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [0.3.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-ga-events-forwarder-browser@0.2.0...@amplitude/plugin-ga-events-forwarder-browser@0.3.0) (2023-08-31) ### Features diff --git a/packages/plugin-ga-events-forwarder-browser/package.json b/packages/plugin-ga-events-forwarder-browser/package.json index c7fbb7fd5..dba70d196 100644 --- a/packages/plugin-ga-events-forwarder-browser/package.json +++ b/packages/plugin-ga-events-forwarder-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-ga-events-forwarder-browser", - "version": "0.3.0", + "version": "0.3.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", diff --git a/packages/plugin-ga-events-forwarder-browser/src/version.ts b/packages/plugin-ga-events-forwarder-browser/src/version.ts index ffd80fd3a..d9a6219bd 100644 --- a/packages/plugin-ga-events-forwarder-browser/src/version.ts +++ b/packages/plugin-ga-events-forwarder-browser/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.3.0'; +export const VERSION = '0.3.1'; diff --git a/packages/plugin-page-view-tracking-browser/CHANGELOG.md b/packages/plugin-page-view-tracking-browser/CHANGELOG.md index 8cfab4f41..75c1454d8 100644 --- a/packages/plugin-page-view-tracking-browser/CHANGELOG.md +++ b/packages/plugin-page-view-tracking-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.11](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.10...@amplitude/plugin-page-view-tracking-browser@1.0.11) (2023-10-26) + +**Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.10](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-page-view-tracking-browser@1.0.9...@amplitude/plugin-page-view-tracking-browser@1.0.10) (2023-08-22) **Note:** Version bump only for package @amplitude/plugin-page-view-tracking-browser diff --git a/packages/plugin-page-view-tracking-browser/package.json b/packages/plugin-page-view-tracking-browser/package.json index 26326206f..69e92501a 100644 --- a/packages/plugin-page-view-tracking-browser/package.json +++ b/packages/plugin-page-view-tracking-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-page-view-tracking-browser", - "version": "1.0.10", + "version": "1.0.11", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -37,7 +37,7 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.2.0", + "@amplitude/analytics-client-common": "^1.2.1", "@amplitude/analytics-types": "^1.3.3", "tslib": "^2.4.1" }, diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 194576179..b32fb672f 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.6.16](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.15...@amplitude/plugin-session-replay-browser@0.6.16) (2023-10-26) + +### Bug Fixes + +- **session replay:** use internal fork of rrweb + ([8f96d1c](https://github.com/amplitude/Amplitude-TypeScript/commit/8f96d1ceb7d31dabd2fc8c34aba3d01cfa886f79)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.6.15](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.14...@amplitude/plugin-session-replay-browser@0.6.15) (2023-10-17) **Note:** Version bump only for package @amplitude/plugin-session-replay-browser diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 623832553..348ccb8b5 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.6.15", + "version": "0.6.16", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -40,7 +40,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "^0.3.0", + "@amplitude/session-replay-browser": "^0.3.1", "idb-keyval": "^6.2.1", "tslib": "^2.4.1" }, diff --git a/packages/plugin-web-attribution-browser/CHANGELOG.md b/packages/plugin-web-attribution-browser/CHANGELOG.md index 09be68405..4348721e6 100644 --- a/packages/plugin-web-attribution-browser/CHANGELOG.md +++ b/packages/plugin-web-attribution-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.11](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.10...@amplitude/plugin-web-attribution-browser@1.0.11) (2023-10-26) + +**Note:** Version bump only for package @amplitude/plugin-web-attribution-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.10](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-web-attribution-browser@1.0.9...@amplitude/plugin-web-attribution-browser@1.0.10) (2023-08-22) **Note:** Version bump only for package @amplitude/plugin-web-attribution-browser diff --git a/packages/plugin-web-attribution-browser/package.json b/packages/plugin-web-attribution-browser/package.json index 44d41028c..50b3a591f 100644 --- a/packages/plugin-web-attribution-browser/package.json +++ b/packages/plugin-web-attribution-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-web-attribution-browser", - "version": "1.0.10", + "version": "1.0.11", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -37,8 +37,8 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^1.2.0", - "@amplitude/analytics-core": "^1.2.3", + "@amplitude/analytics-client-common": "^1.2.1", + "@amplitude/analytics-core": "^1.2.4", "@amplitude/analytics-types": "^1.3.3", "tslib": "^2.4.1" }, diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index d85b5d803..e58ac0629 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.3.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.3.0...@amplitude/session-replay-browser@0.3.1) (2023-10-26) + +### Bug Fixes + +- **session replay:** use internal fork of rrweb + ([8f96d1c](https://github.com/amplitude/Amplitude-TypeScript/commit/8f96d1ceb7d31dabd2fc8c34aba3d01cfa886f79)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [0.3.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.2.5...@amplitude/session-replay-browser@0.3.0) (2023-10-17) ### Features diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index bf3f7e018..6ce63ebb6 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "0.3.0", + "version": "0.3.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 404d09ecc7a7923d7016fc2de3df26af442e123d Mon Sep 17 00:00:00 2001 From: Marvin Liu Date: Thu, 26 Oct 2023 16:40:55 -0700 Subject: [PATCH 188/214] feat: make auto-tracking solely use css selector allowlist (#606) * feat: make auto-tracking solely use css selector allowlist * feat: add default allowlist class * docs: update readme --- .../README.md | 2 +- .../default-event-tracking-advanced-plugin.ts | 50 ++++---- .../src/helpers.ts | 16 ++- .../src/index.ts | 2 + .../default-event-tracking-advanced.test.ts | 114 ++++++++++++++---- .../test/helpers.test.ts | 20 +++ 6 files changed, 155 insertions(+), 49 deletions(-) diff --git a/packages/plugin-default-event-tracking-advanced-browser/README.md b/packages/plugin-default-event-tracking-advanced-browser/README.md index 61716fc85..560da6a6b 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/README.md +++ b/packages/plugin-default-event-tracking-advanced-browser/README.md @@ -65,7 +65,7 @@ Examples: |Name|Type|Default|Description| |-|-|-|-| -|`cssSelectorAllowlist`|`string[]`|`undefined`| When provided, only allow elements matching any selector to be tracked. | +|`cssSelectorAllowlist`|`string[]`|`['a', 'button', 'input', 'select', 'textarea', 'label', '.amp-default-track']`| When provided, only allow elements matching any selector to be tracked. | |`pageUrlAllowlist`|`(string\|RegExp)[]`|`undefined`| When provided, only allow elements matching URLs to be tracked. | |`shouldTrackEventResolver`|`(actionType: ActionType, element: Element) => boolean`|`undefined`| When provided, overwrite the default filter behavior. | |`dataAttributePrefix`|`string`|`'data-amp-track-'`| Allow data attributes to be collected in event property. | diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts index b3246d6b3..e072d4334 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts @@ -1,19 +1,34 @@ /* eslint-disable no-restricted-globals */ import { BrowserClient, BrowserConfig, EnrichmentPlugin } from '@amplitude/analytics-types'; import * as constants from './constants'; -import { getText, isPageUrlAllowed, getAttributesWithPrefix, removeEmptyProperties, getNearestLabel } from './helpers'; +import { + getText, + isPageUrlAllowed, + getAttributesWithPrefix, + removeEmptyProperties, + getNearestLabel, + querySelectUniqueElements, +} from './helpers'; import { finder } from './libs/finder'; type BrowserEnrichmentPlugin = EnrichmentPlugin; type ActionType = 'click' | 'change'; -const DEFAULT_TAG_ALLOWLIST = ['a', 'button', 'input', 'select', 'textarea', 'label']; -const DEFAULT_DATA_ATTRIBUTE_PREFIX = 'data-amp-track-'; +export const DEFAULT_CSS_SELECTOR_ALLOWLIST = [ + 'a', + 'button', + 'input', + 'select', + 'textarea', + 'label', + '.amp-default-track', +]; +export const DEFAULT_DATA_ATTRIBUTE_PREFIX = 'data-amp-track-'; interface EventListener { element: Element; type: ActionType; - handler: () => void; + handler: (event: Event) => void; } interface Options { @@ -50,7 +65,7 @@ interface Options { export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): BrowserEnrichmentPlugin => { const { - cssSelectorAllowlist, + cssSelectorAllowlist = DEFAULT_CSS_SELECTOR_ALLOWLIST, pageUrlAllowlist, shouldTrackEventResolver, dataAttributePrefix = DEFAULT_DATA_ATTRIBUTE_PREFIX, @@ -61,7 +76,7 @@ export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): Brows let observer: MutationObserver | undefined; let eventListeners: EventListener[] = []; - const addEventListener = (element: Element, type: ActionType, handler: () => void) => { + const addEventListener = (element: Element, type: ActionType, handler: (event: Event) => void) => { element.addEventListener(type, handler); eventListeners.push({ element: element, @@ -108,19 +123,6 @@ export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): Brows /* istanbul ignore next */ const tag = element?.tagName?.toLowerCase?.(); - // We only track these limited tags, as general text tag is not interactive and might contain sensitive information. - /* istanbul ignore if */ - if (!DEFAULT_TAG_ALLOWLIST.includes(tag)) { - return false; - } - - /* istanbul ignore if */ - if (cssSelectorAllowlist) { - const hasMatchAnyAllowedSelector = cssSelectorAllowlist.some((selector) => element.matches(selector)); - if (!hasMatchAnyAllowedSelector) { - return false; - } - } switch (tag) { case 'input': case 'select': @@ -184,19 +186,21 @@ export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): Brows } const addListener = (el: Element) => { if (shouldTrackEvent('click', el)) { - addEventListener(el, 'click', () => { + addEventListener(el, 'click', (event: Event) => { /* istanbul ignore next */ amplitude?.track(constants.AMPLITUDE_ELEMENT_CLICKED_EVENT, getEventProperties('click', el)); + event.stopPropagation(); }); } if (shouldTrackEvent('change', el)) { - addEventListener(el, 'change', () => { + addEventListener(el, 'change', (event: Event) => { /* istanbul ignore next */ amplitude?.track(constants.AMPLITUDE_ELEMENT_CHANGED_EVENT, getEventProperties('change', el)); + event.stopPropagation(); }); } }; - const allElements = Array.from(document.body.querySelectorAll(DEFAULT_TAG_ALLOWLIST.join(','))); + const allElements = querySelectUniqueElements(document.body, cssSelectorAllowlist); allElements.forEach(addListener); if (typeof MutationObserver !== 'undefined') { observer = new MutationObserver((mutations) => { @@ -204,7 +208,7 @@ export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): Brows mutation.addedNodes.forEach((node: Node) => { addListener(node as Element); if ('querySelectorAll' in node && typeof node.querySelectorAll === 'function') { - Array.from(node.querySelectorAll(DEFAULT_TAG_ALLOWLIST.join(',')) as HTMLElement[]).map(addListener); + querySelectUniqueElements(node as Element, cssSelectorAllowlist).map(addListener); } }); }); diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts b/packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts index 2a355a292..fa82a17dc 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts @@ -23,7 +23,8 @@ export const isTextNode = (node: Node) => { }; export const isNonSensitiveElement = (element: Element) => { - const tag = element.tagName.toLowerCase(); + /* istanbul ignore next */ + const tag = element?.tagName?.toLowerCase?.(); return !SENTITIVE_TAGS.includes(tag); }; @@ -109,3 +110,16 @@ export const getNearestLabel = (element: Element): string => { } return getNearestLabel(parent); }; + +export const querySelectUniqueElements = (root: Element | Document, selectors: string[]): Element[] => { + const elementSet = selectors.reduce((elements: Set, selector) => { + if (selector) { + const selectedElements = Array.from(root.querySelectorAll(selector)); + selectedElements.forEach((element) => { + elements.add(element); + }); + } + return elements; + }, new Set()); + return Array.from(elementSet); +}; diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/index.ts b/packages/plugin-default-event-tracking-advanced-browser/src/index.ts index 7ab523eef..1e72a37f6 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/src/index.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/index.ts @@ -1,4 +1,6 @@ export { defaultEventTrackingAdvancedPlugin as plugin, defaultEventTrackingAdvancedPlugin, + DEFAULT_CSS_SELECTOR_ALLOWLIST, + DEFAULT_DATA_ATTRIBUTE_PREFIX, } from './default-event-tracking-advanced-plugin'; diff --git a/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts b/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts index 36bb91f80..e4b78d1f6 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts @@ -36,6 +36,7 @@ describe('autoTrackingPlugin', () => { afterEach(() => { void plugin?.teardown?.(); + document.getElementsByTagName('body')[0].innerHTML = ''; jest.clearAllMocks(); }); @@ -248,32 +249,97 @@ describe('autoTrackingPlugin', () => { expect(track).toHaveBeenCalledTimes(1); }); - test('should follow cssSelectorAllowlist configuration', async () => { - const button = document.createElement('button'); - const buttonText = document.createTextNode('submit'); - button.setAttribute('id', 'my-button-id'); - button.setAttribute('class', 'my-button-class'); - button.appendChild(buttonText); - document.body.appendChild(button); - - plugin = defaultEventTrackingAdvancedPlugin({ cssSelectorAllowlist: ['.my-button-class'] }); - const loggerProvider: Partial = { - log: jest.fn(), - warn: jest.fn(), - }; - const config: Partial = { - defaultTracking: false, - loggerProvider: loggerProvider as Logger, - }; - await plugin?.setup(config as BrowserConfig, instance); + describe('cssSelectorAllowlist configuration', () => { + test('should only track selector class', async () => { + const button = document.createElement('button'); + const buttonText = document.createTextNode('submit'); + button.setAttribute('id', 'my-button-id'); + button.setAttribute('class', 'my-button-class'); + button.appendChild(buttonText); + document.body.appendChild(button); + + plugin = defaultEventTrackingAdvancedPlugin({ cssSelectorAllowlist: ['.my-button-class'] }); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click link + document.getElementById('my-link-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(0); + + // trigger click button + document.getElementById('my-button-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(1); + }); - // trigger click link - document.getElementById('my-link-id')?.dispatchEvent(new Event('click')); - expect(track).toHaveBeenCalledTimes(0); + test('should be able to track non-default tags by overwriting default cssSelectorAllowlist', async () => { + const div = document.createElement('div'); + div.textContent = 'my-div-text'; + div.setAttribute('id', 'my-div-id'); + document.body.appendChild(div); + + const button = document.createElement('button'); + button.textContent = 'my-button-text'; + button.setAttribute('id', 'my-button-id'); + document.body.appendChild(button); + + plugin = defaultEventTrackingAdvancedPlugin({ cssSelectorAllowlist: ['div'] }); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click button + document.getElementById('my-button-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(0); + + // trigger click div + document.getElementById('my-div-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(1); + }); - // trigger click button - document.getElementById('my-button-id')?.dispatchEvent(new Event('click')); - expect(track).toHaveBeenCalledTimes(1); + test('should respect default cssSelectorAllowlist', async () => { + const div = document.createElement('div'); + div.textContent = 'my-div-text'; + div.setAttribute('id', 'my-div-id'); + div.className = 'amp-default-track'; // default css class to enable tracking + document.body.appendChild(div); + + const button = document.createElement('button'); + button.textContent = 'my-button-text'; + button.setAttribute('id', 'my-button-id'); + document.body.appendChild(button); + + plugin = defaultEventTrackingAdvancedPlugin(); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click button + document.getElementById('my-button-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(1); + + // trigger click div + document.getElementById('my-div-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(2); + }); }); test('should follow pageUrlAllowlist configuration', async () => { diff --git a/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts b/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts index a109ae70a..b7f15d745 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts @@ -8,6 +8,7 @@ import { isEmpty, removeEmptyProperties, getNearestLabel, + querySelectUniqueElements, } from '../src/helpers'; describe('default-event-tracking-advanced-plugin helpers', () => { @@ -316,4 +317,23 @@ describe('default-event-tracking-advanced-plugin helpers', () => { expect(result).toEqual(''); }); }); + + describe('querySelectUniqueElements', () => { + test('should return unique elements with selector under root', () => { + const container = document.createElement('div'); + + const div1 = document.createElement('div'); + div1.className = 'test-class'; + container.appendChild(div1); + const div2 = document.createElement('div'); + container.appendChild(div2); + + let result = querySelectUniqueElements(container, ['div']); + expect(result).toEqual([div1, div2]); + + // elements should be deduped + result = querySelectUniqueElements(container, ['div', '.test-class']); + expect(result).toEqual([div1, div2]); + }); + }); }); From 517fa48716dcd102dfb9ee43332016da62676dce Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Fri, 27 Oct 2023 00:31:01 +0000 Subject: [PATCH 189/214] chore(release): publish - @amplitude/plugin-default-event-tracking-advanced-browser@0.1.0 --- .../CHANGELOG.md | 13 +++++++++++++ .../package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md index ca1542daf..baa19fffe 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md +++ b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.1.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.0.2...@amplitude/plugin-default-event-tracking-advanced-browser@0.1.0) (2023-10-27) + +### Features + +- make auto-tracking solely use css selector allowlist + ([#606](https://github.com/amplitude/Amplitude-TypeScript/issues/606)) + ([404d09e](https://github.com/amplitude/Amplitude-TypeScript/commit/404d09ecc7a7923d7016fc2de3df26af442e123d)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.0.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.0.1...@amplitude/plugin-default-event-tracking-advanced-browser@0.0.2) (2023-10-26) **Note:** Version bump only for package @amplitude/plugin-default-event-tracking-advanced-browser diff --git a/packages/plugin-default-event-tracking-advanced-browser/package.json b/packages/plugin-default-event-tracking-advanced-browser/package.json index 15f95800b..edd5632dc 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/package.json +++ b/packages/plugin-default-event-tracking-advanced-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-default-event-tracking-advanced-browser", - "version": "0.0.2", + "version": "0.1.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From afe1cc2567b2bcc50a3416299b16f4721a50866a Mon Sep 17 00:00:00 2001 From: Marvin Liu Date: Fri, 27 Oct 2023 13:02:37 -0700 Subject: [PATCH 190/214] fix: remove event stopPropagation (#608) --- .../src/default-event-tracking-advanced-plugin.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts index e072d4334..902515f9d 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts @@ -186,17 +186,15 @@ export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): Brows } const addListener = (el: Element) => { if (shouldTrackEvent('click', el)) { - addEventListener(el, 'click', (event: Event) => { + addEventListener(el, 'click', () => { /* istanbul ignore next */ amplitude?.track(constants.AMPLITUDE_ELEMENT_CLICKED_EVENT, getEventProperties('click', el)); - event.stopPropagation(); }); } if (shouldTrackEvent('change', el)) { - addEventListener(el, 'change', (event: Event) => { + addEventListener(el, 'change', () => { /* istanbul ignore next */ amplitude?.track(constants.AMPLITUDE_ELEMENT_CHANGED_EVENT, getEventProperties('change', el)); - event.stopPropagation(); }); } }; From e60ab0af014ab5f24779d393b67fc5206dc68339 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Fri, 27 Oct 2023 20:12:36 +0000 Subject: [PATCH 191/214] chore(release): publish - @amplitude/plugin-default-event-tracking-advanced-browser@0.1.1 --- .../CHANGELOG.md | 12 ++++++++++++ .../package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md index baa19fffe..f7f6298d2 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md +++ b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.1.0...@amplitude/plugin-default-event-tracking-advanced-browser@0.1.1) (2023-10-27) + +### Bug Fixes + +- remove event stopPropagation ([#608](https://github.com/amplitude/Amplitude-TypeScript/issues/608)) + ([afe1cc2](https://github.com/amplitude/Amplitude-TypeScript/commit/afe1cc2567b2bcc50a3416299b16f4721a50866a)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [0.1.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.0.2...@amplitude/plugin-default-event-tracking-advanced-browser@0.1.0) (2023-10-27) ### Features diff --git a/packages/plugin-default-event-tracking-advanced-browser/package.json b/packages/plugin-default-event-tracking-advanced-browser/package.json index edd5632dc..d7e18fc42 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/package.json +++ b/packages/plugin-default-event-tracking-advanced-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-default-event-tracking-advanced-browser", - "version": "0.1.0", + "version": "0.1.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 92c412b106158e57d57d526c05df0ec23977b388 Mon Sep 17 00:00:00 2001 From: Marvin Liu Date: Fri, 27 Oct 2023 14:07:49 -0700 Subject: [PATCH 192/214] fix: add filter back to handle current node (#609) --- .../src/default-event-tracking-advanced-plugin.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts index 902515f9d..59f4dc3df 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts @@ -123,6 +123,14 @@ export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): Brows /* istanbul ignore next */ const tag = element?.tagName?.toLowerCase?.(); + /* istanbul ignore if */ + if (cssSelectorAllowlist) { + const hasMatchAnyAllowedSelector = cssSelectorAllowlist.some((selector) => element.matches(selector)); + if (!hasMatchAnyAllowedSelector) { + return false; + } + } + switch (tag) { case 'input': case 'select': From 2b421dc36c5a823851aacdc489cf582f8d66d2e1 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Fri, 27 Oct 2023 21:16:10 +0000 Subject: [PATCH 193/214] chore(release): publish - @amplitude/plugin-default-event-tracking-advanced-browser@0.1.2 --- .../CHANGELOG.md | 12 ++++++++++++ .../package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md index f7f6298d2..d1fa9a53b 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md +++ b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.1.1...@amplitude/plugin-default-event-tracking-advanced-browser@0.1.2) (2023-10-27) + +### Bug Fixes + +- add filter back to handle current node ([#609](https://github.com/amplitude/Amplitude-TypeScript/issues/609)) + ([92c412b](https://github.com/amplitude/Amplitude-TypeScript/commit/92c412b106158e57d57d526c05df0ec23977b388)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.1.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.1.0...@amplitude/plugin-default-event-tracking-advanced-browser@0.1.1) (2023-10-27) ### Bug Fixes diff --git a/packages/plugin-default-event-tracking-advanced-browser/package.json b/packages/plugin-default-event-tracking-advanced-browser/package.json index d7e18fc42..1e595020a 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/package.json +++ b/packages/plugin-default-event-tracking-advanced-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-default-event-tracking-advanced-browser", - "version": "0.1.1", + "version": "0.1.2", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From b425d441753b298cc3a6717b00e38230612f19a3 Mon Sep 17 00:00:00 2001 From: Marvin Liu Date: Fri, 27 Oct 2023 16:15:50 -0700 Subject: [PATCH 194/214] fix: handle text node throw error issue (#610) --- .../default-event-tracking-advanced-plugin.ts | 11 +++++--- .../default-event-tracking-advanced.test.ts | 27 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts index 59f4dc3df..f31bbafe0 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts @@ -102,6 +102,13 @@ export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): Brows return false; } + /* istanbul ignore next */ + const tag = element?.tagName?.toLowerCase?.(); + // Text nodes have no tag + if (!tag) { + return false; + } + if (shouldTrackEventResolver) { return shouldTrackEventResolver(actionType, element); } @@ -120,12 +127,10 @@ export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): Brows return false; } } - /* istanbul ignore next */ - const tag = element?.tagName?.toLowerCase?.(); /* istanbul ignore if */ if (cssSelectorAllowlist) { - const hasMatchAnyAllowedSelector = cssSelectorAllowlist.some((selector) => element.matches(selector)); + const hasMatchAnyAllowedSelector = cssSelectorAllowlist.some((selector) => !!element?.matches?.(selector)); if (!hasMatchAnyAllowedSelector) { return false; } diff --git a/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts b/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts index e4b78d1f6..5f2758f1d 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts @@ -561,6 +561,33 @@ describe('autoTrackingPlugin', () => { document.getElementById('my-div-id')?.dispatchEvent(new Event('change')); expect(track).toHaveBeenCalledTimes(0); }); + + test('should not throw error when there is text node added to the page', async () => { + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + const textNode = document.createTextNode('Some text node'); + document.body.appendChild(textNode); + + const div = document.createElement('div'); + div.setAttribute('id', 'my-div-id'); + div.setAttribute('class', 'my-div-class'); + document.body.appendChild(div); + + // allow mutation observer to execute and event listener to be attached + await new Promise((r) => r(undefined)); // basically, await next clock tick + + // trigger click input + document.getElementById('my-div-id')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(0); + }); }); describe('teardown', () => { From ab0a91a40e7d76949283649f04de7b31fb60770a Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Fri, 27 Oct 2023 23:28:44 +0000 Subject: [PATCH 195/214] chore(release): publish - @amplitude/plugin-default-event-tracking-advanced-browser@0.1.3 --- .../CHANGELOG.md | 12 ++++++++++++ .../package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md index d1fa9a53b..9043bc48c 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md +++ b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.1.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.1.2...@amplitude/plugin-default-event-tracking-advanced-browser@0.1.3) (2023-10-27) + +### Bug Fixes + +- handle text node throw error issue ([#610](https://github.com/amplitude/Amplitude-TypeScript/issues/610)) + ([b425d44](https://github.com/amplitude/Amplitude-TypeScript/commit/b425d441753b298cc3a6717b00e38230612f19a3)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.1.2](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.1.1...@amplitude/plugin-default-event-tracking-advanced-browser@0.1.2) (2023-10-27) ### Bug Fixes diff --git a/packages/plugin-default-event-tracking-advanced-browser/package.json b/packages/plugin-default-event-tracking-advanced-browser/package.json index 1e595020a..74b47f5f6 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/package.json +++ b/packages/plugin-default-event-tracking-advanced-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-default-event-tracking-advanced-browser", - "version": "0.1.2", + "version": "0.1.3", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 03a67c97e6c5589662be0a1a7e4cd7a725774d85 Mon Sep 17 00:00:00 2001 From: Jesse Wang Date: Tue, 31 Oct 2023 11:42:10 -0700 Subject: [PATCH 196/214] feat: add session replay ID generic identifier to init/turnover --- packages/session-replay-browser/src/config.ts | 11 +++++++++-- .../session-replay-browser/src/constants.ts | 1 + .../session-replay-browser/src/helpers.ts | 4 ++++ .../src/session-replay.ts | 19 ++++++++++++++++--- .../src/typings/session-replay.ts | 7 ++++--- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/packages/session-replay-browser/src/config.ts b/packages/session-replay-browser/src/config.ts index 8acd5dc28..2a1c553e0 100644 --- a/packages/session-replay-browser/src/config.ts +++ b/packages/session-replay-browser/src/config.ts @@ -3,6 +3,7 @@ import { Config, Logger } from '@amplitude/analytics-core'; import { LogLevel } from '@amplitude/analytics-types'; import { SessionReplayConfig as ISessionReplayConfig, SessionReplayOptions } from './typings/session-replay'; import { DEFAULT_SAMPLE_RATE } from './constants'; +import { generateSessionReplayId } from './helpers'; export const getDefaultConfig = () => ({ flushMaxRetries: 2, @@ -16,6 +17,7 @@ export class SessionReplayConfig extends Config implements ISessionReplayConfig sampleRate: number; deviceId?: string | undefined; sessionId?: number | undefined; + sessionReplayId?: string | undefined | null; constructor(apiKey: string, options: SessionReplayOptions) { const defaultConfig = getDefaultConfig(); @@ -32,7 +34,12 @@ export class SessionReplayConfig extends Config implements ISessionReplayConfig this.apiKey = apiKey; this.sampleRate = options.sampleRate || DEFAULT_SAMPLE_RATE; - this.deviceId = options.deviceId; - this.sessionId = options.sessionId; + if (options.sessionReplayId) { + this.sessionReplayId = options.sessionReplayId; + } else if (options.sessionId && options.deviceId) { + this.deviceId = options.deviceId; + this.sessionId = options.sessionId; + this.sessionReplayId = generateSessionReplayId(options.sessionId, options.deviceId); + } } } diff --git a/packages/session-replay-browser/src/constants.ts b/packages/session-replay-browser/src/constants.ts index 415860376..d55122d65 100644 --- a/packages/session-replay-browser/src/constants.ts +++ b/packages/session-replay-browser/src/constants.ts @@ -3,6 +3,7 @@ import { IDBStoreSession } from './typings/session-replay'; export const DEFAULT_EVENT_PROPERTY_PREFIX = '[Amplitude]'; +export const NEW_SESSION_REPLAY_PROPERTY = `${DEFAULT_EVENT_PROPERTY_PREFIX} Session Replay ID`; export const DEFAULT_SESSION_REPLAY_PROPERTY = `${DEFAULT_EVENT_PROPERTY_PREFIX} Session Recorded`; export const DEFAULT_SESSION_START_EVENT = 'session_start'; export const DEFAULT_SESSION_END_EVENT = 'session_end'; diff --git a/packages/session-replay-browser/src/helpers.ts b/packages/session-replay-browser/src/helpers.ts index d4581e788..86027e470 100644 --- a/packages/session-replay-browser/src/helpers.ts +++ b/packages/session-replay-browser/src/helpers.ts @@ -31,3 +31,7 @@ export const getCurrentUrl = () => { const globalScope = getGlobalScope(); return globalScope?.location ? globalScope.location.href : ''; }; + +export const generateSessionReplayId = (sessionId: number, deviceId: string): string => { + return `${sessionId}/${deviceId}`; +}; diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index 4e1c079f7..bcd0754a2 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -13,12 +13,13 @@ import { MAX_IDB_STORAGE_LENGTH, MAX_INTERVAL, MIN_INTERVAL, + NEW_SESSION_REPLAY_PROPERTY, SESSION_REPLAY_EU_URL as SESSION_REPLAY_EU_SERVER_URL, SESSION_REPLAY_SERVER_URL, STORAGE_PREFIX, defaultSessionStore, } from './constants'; -import { getCurrentUrl, isSessionInSample, maskInputFn } from './helpers'; +import { isSessionInSample, maskInputFn, getCurrentUrl, generateSessionReplayId } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, MISSING_API_KEY_MESSAGE, @@ -82,13 +83,24 @@ export class SessionReplay implements AmplitudeSessionReplay { } } - setSessionId(sessionId: number) { + setSessionId(sessionId?: number, sessionReplayId?: string) { if (!this.config) { this.loggerProvider.error('Session replay init has not been called, cannot set session id.'); return; } - this.stopRecordingAndSendEvents(this.config.sessionId); + // const lastSessionReplayId = this.config.sessionReplayId; this.config.sessionId = sessionId; + if (sessionId && this.config.deviceId) { + this.config.sessionReplayId = generateSessionReplayId(sessionId, this.config.deviceId); + } else if (sessionReplayId) { + this.config.sessionReplayId = sessionReplayId; + } else { + this.loggerProvider.error('Must provide either session replay id or session id when starting a new session.'); + return; + } + + // Use lastSessionReplayId + this.stopRecordingAndSendEvents(this.config.sessionId); this.events = []; this.currentSequenceId = 0; this.recordEvents(); @@ -104,6 +116,7 @@ export class SessionReplay implements AmplitudeSessionReplay { if (shouldRecord) { return { [DEFAULT_SESSION_REPLAY_PROPERTY]: true, + [NEW_SESSION_REPLAY_PROPERTY]: this.config.sessionReplayId ? this.config.sessionReplayId : null, }; } diff --git a/packages/session-replay-browser/src/typings/session-replay.ts b/packages/session-replay-browser/src/typings/session-replay.ts index 9a489a2ef..423e89969 100644 --- a/packages/session-replay-browser/src/typings/session-replay.ts +++ b/packages/session-replay-browser/src/typings/session-replay.ts @@ -39,14 +39,15 @@ export interface SessionReplayConfig extends Config { logLevel: LogLevel; flushMaxRetries: number; sampleRate: number; + sessionReplayId?: string | null; } export type SessionReplayOptions = Omit, 'apiKey'>; export interface AmplitudeSessionReplay { init: (apiKey: string, options: SessionReplayOptions) => AmplitudeReturn; - setSessionId: (sessionId: number) => void; - getSessionRecordingProperties: () => { [key: string]: boolean }; - getSessionReplayProperties: () => { [key: string]: boolean }; + setSessionId: (sessionId?: number, sessionReplayId?: string) => void; + getSessionRecordingProperties: () => { [key: string]: boolean | string | null }; + getSessionReplayProperties: () => { [key: string]: boolean | string | null }; shutdown: () => void; } From 990ddaf102465e52d84e496d280c8aada5f94944 Mon Sep 17 00:00:00 2001 From: Jesse Wang Date: Tue, 31 Oct 2023 16:24:12 -0700 Subject: [PATCH 197/214] feat(session replay): parse sessionReplayId for sessionId and deviceId --- .../src/session-replay.ts | 2 +- packages/session-replay-browser/src/config.ts | 2 +- .../session-replay-browser/src/helpers.ts | 18 ++++++++++++++- .../src/session-replay.ts | 22 +++++++++++++------ .../src/typings/session-replay.ts | 4 ++-- 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 10d246b1d..1a6812ba3 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -51,7 +51,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { async execute(event: Event) { if (event.event_type === DEFAULT_SESSION_START_EVENT && event.session_id) { - sessionReplay.setSessionId(event.session_id); + sessionReplay.setSessionId({ sessionId: event.session_id }); } const sessionRecordingProperties = sessionReplay.getSessionReplayProperties(); diff --git a/packages/session-replay-browser/src/config.ts b/packages/session-replay-browser/src/config.ts index 2a1c553e0..ccf75c833 100644 --- a/packages/session-replay-browser/src/config.ts +++ b/packages/session-replay-browser/src/config.ts @@ -17,7 +17,7 @@ export class SessionReplayConfig extends Config implements ISessionReplayConfig sampleRate: number; deviceId?: string | undefined; sessionId?: number | undefined; - sessionReplayId?: string | undefined | null; + sessionReplayId?: string | undefined; constructor(apiKey: string, options: SessionReplayOptions) { const defaultConfig = getDefaultConfig(); diff --git a/packages/session-replay-browser/src/helpers.ts b/packages/session-replay-browser/src/helpers.ts index 86027e470..1295c7928 100644 --- a/packages/session-replay-browser/src/helpers.ts +++ b/packages/session-replay-browser/src/helpers.ts @@ -33,5 +33,21 @@ export const getCurrentUrl = () => { }; export const generateSessionReplayId = (sessionId: number, deviceId: string): string => { - return `${sessionId}/${deviceId}`; + return `${deviceId}/${sessionId}`; +}; + +export const parseSessionReplayId = ( + sessionReplayId: string | undefined, +): { deviceId?: string; sessionId?: string } => { + if (!sessionReplayId) { + return {}; + } + + const parts = sessionReplayId.split('/'); + if (parts.length === 2) { + const [deviceId, sessionId] = sessionReplayId.split('/'); + return { deviceId, sessionId }; + } + + return {}; }; diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index bcd0754a2..569659002 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -19,7 +19,13 @@ import { STORAGE_PREFIX, defaultSessionStore, } from './constants'; -import { isSessionInSample, maskInputFn, getCurrentUrl, generateSessionReplayId } from './helpers'; +import { + isSessionInSample, + maskInputFn, + getCurrentUrl, + generateSessionReplayId, + parseSessionReplayId, +} from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, MISSING_API_KEY_MESSAGE, @@ -83,12 +89,13 @@ export class SessionReplay implements AmplitudeSessionReplay { } } - setSessionId(sessionId?: number, sessionReplayId?: string) { + setSessionId({ sessionId, sessionReplayId }: { sessionId?: number; sessionReplayId?: string }) { if (!this.config) { this.loggerProvider.error('Session replay init has not been called, cannot set session id.'); return; } - // const lastSessionReplayId = this.config.sessionReplayId; + + const lastSessionReplayId = this.config.sessionReplayId; this.config.sessionId = sessionId; if (sessionId && this.config.deviceId) { this.config.sessionReplayId = generateSessionReplayId(sessionId, this.config.deviceId); @@ -99,8 +106,7 @@ export class SessionReplay implements AmplitudeSessionReplay { return; } - // Use lastSessionReplayId - this.stopRecordingAndSendEvents(this.config.sessionId); + this.stopRecordingAndSendEvents(lastSessionReplayId); this.events = []; this.currentSequenceId = 0; this.recordEvents(); @@ -136,7 +142,7 @@ export class SessionReplay implements AmplitudeSessionReplay { void this.initialize(); }; - stopRecordingAndSendEvents(sessionId?: number) { + stopRecordingAndSendEvents(sessionReplayId?: string) { try { this.stopRecordingEvents && this.stopRecordingEvents(); this.stopRecordingEvents = null; @@ -144,7 +150,9 @@ export class SessionReplay implements AmplitudeSessionReplay { const typedError = error as Error; this.loggerProvider.warn(`Error occurred while stopping recording: ${typedError.toString()}`); } - const sessionIdToSend = sessionId || this.config?.sessionId; + + const parseSessionId = parseSessionReplayId(sessionReplayId)?.sessionId; + const sessionIdToSend = parseSessionId ? +parseSessionId : this.config?.sessionId; if (this.events.length && sessionIdToSend) { this.sendEventsList({ events: this.events, diff --git a/packages/session-replay-browser/src/typings/session-replay.ts b/packages/session-replay-browser/src/typings/session-replay.ts index 423e89969..e053a435f 100644 --- a/packages/session-replay-browser/src/typings/session-replay.ts +++ b/packages/session-replay-browser/src/typings/session-replay.ts @@ -39,14 +39,14 @@ export interface SessionReplayConfig extends Config { logLevel: LogLevel; flushMaxRetries: number; sampleRate: number; - sessionReplayId?: string | null; + sessionReplayId?: string; } export type SessionReplayOptions = Omit, 'apiKey'>; export interface AmplitudeSessionReplay { init: (apiKey: string, options: SessionReplayOptions) => AmplitudeReturn; - setSessionId: (sessionId?: number, sessionReplayId?: string) => void; + setSessionId: ({ sessionId, sessionReplayId }: { sessionId?: number; sessionReplayId?: string }) => void; getSessionRecordingProperties: () => { [key: string]: boolean | string | null }; getSessionReplayProperties: () => { [key: string]: boolean | string | null }; shutdown: () => void; From 60f6b04bbe5069f97279c67098ef6a8df259b678 Mon Sep 17 00:00:00 2001 From: Jesse Wang Date: Wed, 1 Nov 2023 09:12:56 -0700 Subject: [PATCH 198/214] fix(session replay): testing changes for sessionReplayId --- .../src/session-replay.ts | 2 +- .../test/helpers.test.ts | 16 +++- .../test/session-replay.test.ts | 89 +++++++++++++++++-- 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index 569659002..86b0b8cd4 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -151,7 +151,7 @@ export class SessionReplay implements AmplitudeSessionReplay { this.loggerProvider.warn(`Error occurred while stopping recording: ${typedError.toString()}`); } - const parseSessionId = parseSessionReplayId(sessionReplayId)?.sessionId; + const parseSessionId = parseSessionReplayId(sessionReplayId).sessionId; const sessionIdToSend = parseSessionId ? +parseSessionId : this.config?.sessionId; if (this.events.length && sessionIdToSend) { this.sendEventsList({ diff --git a/packages/session-replay-browser/test/helpers.test.ts b/packages/session-replay-browser/test/helpers.test.ts index a51e63faa..6748649be 100644 --- a/packages/session-replay-browser/test/helpers.test.ts +++ b/packages/session-replay-browser/test/helpers.test.ts @@ -1,5 +1,5 @@ import { UNMASK_TEXT_CLASS } from '../src/constants'; -import { generateHashCode, isSessionInSample, maskInputFn } from '../src/helpers'; +import { generateHashCode, isSessionInSample, maskInputFn, parseSessionReplayId } from '../src/helpers'; describe('SessionReplayPlugin helpers', () => { describe('maskInputFn', () => { @@ -47,4 +47,18 @@ describe('SessionReplayPlugin helpers', () => { expect(result).toEqual(false); }); }); + + describe('parseSessionReplayId', () => { + test('should return empty object if sessionReplayId is not the correct format', () => { + const sessionReplayId = '123/234/345'; + const result = parseSessionReplayId(sessionReplayId); + expect(result).toEqual({}); + }); + + test('should return empty object if sessionReplayId is undefined', () => { + const sessionReplayId = undefined; + const result = parseSessionReplayId(sessionReplayId); + expect(result).toEqual({}); + }); + }); }); diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index f2f9988ab..21e901cea 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -98,6 +98,21 @@ describe('SessionReplayPlugin', () => { expect(sessionReplay.storageKey).toBe('AMP_replay_unsent_static_key'); }); + test('should setup plugin with sessionReplayId', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { ...mockOptions, sampleRate: 0.5, sessionReplayId: '123/456' }).promise; + expect(sessionReplay.config?.transportProvider).toBeDefined(); + expect(sessionReplay.config?.flushMaxRetries).toBe(1); + expect(sessionReplay.config?.optOut).toBe(false); + expect(sessionReplay.config?.sampleRate).toBe(0.5); + expect(sessionReplay.config?.deviceId).toBe(undefined); + expect(sessionReplay.config?.sessionId).toBe(undefined); + expect(sessionReplay.config?.logLevel).toBe(0); + expect(sessionReplay.config?.sessionReplayId).toBe('123/456'); + expect(sessionReplay.loggerProvider).toBeDefined(); + expect(sessionReplay.storageKey).toBe('AMP_replay_unsent_static_key'); + }); + test('should call initalize with shouldSendStoredEvents=true', async () => { const sessionReplay = new SessionReplay(); const initalize = jest.spyOn(sessionReplay, 'initialize').mockReturnValueOnce(Promise.resolve()); @@ -173,7 +188,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.loggerProvider = mockLoggerProvider; const stopRecordingMock = jest.fn(); - sessionReplay.setSessionId(456); + sessionReplay.setSessionId({ sessionId: 456 }); expect(stopRecordingMock).not.toHaveBeenCalled(); // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockLoggerProvider.error).toHaveBeenCalled(); @@ -187,7 +202,7 @@ describe('SessionReplayPlugin', () => { // Mock class as if it has already been recording events sessionReplay.stopRecordingAndSendEvents = stopRecordingMock; - sessionReplay.setSessionId(456); + sessionReplay.setSessionId({ sessionId: 456 }); expect(stopRecordingMock).toHaveBeenCalled(); }); @@ -201,12 +216,47 @@ describe('SessionReplayPlugin', () => { sessionReplay.events = [mockEventString]; sessionReplay.currentSequenceId = 4; - sessionReplay.setSessionId(456); + sessionReplay.setSessionId({ sessionId: 456 }); expect(stopRecordingMock).toHaveBeenCalled(); expect(sessionReplay.config?.sessionId).toEqual(456); expect(sessionReplay.events).toEqual([]); expect(sessionReplay.currentSequenceId).toEqual(0); }); + + test('should update the session id with session replay id, reset events and current sequence id, and start recording', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { + ...mockOptions, + sessionId: undefined, + deviceId: undefined, + sessionReplayId: '123/456', + }).promise; + const stopRecordingMock = jest.fn(); + + // Mock class as if it has already been recording events + sessionReplay.stopRecordingAndSendEvents = stopRecordingMock; + sessionReplay.events = [mockEventString]; + sessionReplay.currentSequenceId = 4; + + sessionReplay.setSessionId({ sessionReplayId: '456/123' }); + expect(stopRecordingMock).toHaveBeenCalled(); + expect(sessionReplay.config?.sessionId).toEqual(undefined); + expect(sessionReplay.config?.deviceId).toEqual(undefined); + expect(sessionReplay.events).toEqual([]); + expect(sessionReplay.currentSequenceId).toEqual(0); + }); + + test('should return early if no session id, device id, and session replay id', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, mockOptions).promise; + sessionReplay.loggerProvider = mockLoggerProvider; + const stopRecordingMock = jest.fn(); + + sessionReplay.setSessionId({}); + expect(stopRecordingMock).not.toHaveBeenCalled(); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockLoggerProvider.error).toHaveBeenCalled(); + }); }); describe('getSessionReplayProperties', () => { @@ -237,6 +287,34 @@ describe('SessionReplayPlugin', () => { const result = sessionReplay.getSessionReplayProperties(); expect(result).toEqual({ '[Amplitude] Session Recorded': true, + '[Amplitude] Session Replay ID': '1a2b3c/123', + }); + }); + + test('should return session replay id property with session replay id', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { ...mockOptions, sessionReplayId: '123/456' }).promise; + sessionReplay.getShouldRecord = () => true; + + const result = sessionReplay.getSessionReplayProperties(); + expect(result).toEqual({ + '[Amplitude] Session Recorded': true, + '[Amplitude] Session Replay ID': '123/456', + }); + }); + + test('should return session replay id property with null', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { ...mockOptions }).promise; + sessionReplay.getShouldRecord = () => true; + if (sessionReplay.config) { + sessionReplay.config.sessionReplayId = undefined; + } + + const result = sessionReplay.getSessionReplayProperties(); + expect(result).toEqual({ + '[Amplitude] Session Recorded': true, + '[Amplitude] Session Replay ID': null, }); }); }); @@ -276,6 +354,7 @@ describe('SessionReplayPlugin', () => { const result = sessionReplay.getSessionRecordingProperties(); expect(result).toEqual({ '[Amplitude] Session Recorded': true, + '[Amplitude] Session Replay ID': '1a2b3c/123', }); }); }); @@ -659,10 +738,10 @@ describe('SessionReplayPlugin', () => { sessionReplay.sendEventsList = sendEventsListMock; sessionReplay.events = [mockEventString]; sessionReplay.currentSequenceId = 4; - sessionReplay.stopRecordingAndSendEvents(789); + sessionReplay.stopRecordingAndSendEvents('789/123'); expect(sendEventsListMock).toHaveBeenCalledWith({ events: [mockEventString], - sessionId: 789, + sessionId: 123, sequenceId: 4, }); }); From c0f0c2dcbcb43959992380fe26b0df961a4ea9f4 Mon Sep 17 00:00:00 2001 From: Marvin Liu Date: Wed, 1 Nov 2023 13:34:33 -0700 Subject: [PATCH 199/214] feat: add default data attr selector (#612) * feat: add default data attr selector * refactor: adjust the order of data attr to be ahead of the class --- .../README.md | 2 +- .../default-event-tracking-advanced-plugin.ts | 1 + .../default-event-tracking-advanced.test.ts | 24 +++++++++++++------ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/plugin-default-event-tracking-advanced-browser/README.md b/packages/plugin-default-event-tracking-advanced-browser/README.md index 560da6a6b..13369db78 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/README.md +++ b/packages/plugin-default-event-tracking-advanced-browser/README.md @@ -65,7 +65,7 @@ Examples: |Name|Type|Default|Description| |-|-|-|-| -|`cssSelectorAllowlist`|`string[]`|`['a', 'button', 'input', 'select', 'textarea', 'label', '.amp-default-track']`| When provided, only allow elements matching any selector to be tracked. | +|`cssSelectorAllowlist`|`string[]`|`['a', 'button', 'input', 'select', 'textarea', 'label', '[data-amp-default-track]', '.amp-default-track']`| When provided, only allow elements matching any selector to be tracked. | |`pageUrlAllowlist`|`(string\|RegExp)[]`|`undefined`| When provided, only allow elements matching URLs to be tracked. | |`shouldTrackEventResolver`|`(actionType: ActionType, element: Element) => boolean`|`undefined`| When provided, overwrite the default filter behavior. | |`dataAttributePrefix`|`string`|`'data-amp-track-'`| Allow data attributes to be collected in event property. | diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts index f31bbafe0..dc41bad4d 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts @@ -21,6 +21,7 @@ export const DEFAULT_CSS_SELECTOR_ALLOWLIST = [ 'select', 'textarea', 'label', + '[data-amp-default-track]', '.amp-default-track', ]; export const DEFAULT_DATA_ATTRIBUTE_PREFIX = 'data-amp-track-'; diff --git a/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts b/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts index 5f2758f1d..8c7349fc0 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts @@ -310,11 +310,17 @@ describe('autoTrackingPlugin', () => { }); test('should respect default cssSelectorAllowlist', async () => { - const div = document.createElement('div'); - div.textContent = 'my-div-text'; - div.setAttribute('id', 'my-div-id'); - div.className = 'amp-default-track'; // default css class to enable tracking - document.body.appendChild(div); + const div1 = document.createElement('div'); + div1.textContent = 'my-div-text1'; + div1.setAttribute('id', 'my-div-id1'); + div1.className = 'amp-default-track'; // default css class to enable tracking + document.body.appendChild(div1); + + const div2 = document.createElement('div'); + div2.textContent = 'my-div-text2'; + div2.setAttribute('id', 'my-div-id2'); + div2.setAttribute('data-amp-default-track', ''); // default data attribute to enable tracking + document.body.appendChild(div2); const button = document.createElement('button'); button.textContent = 'my-button-text'; @@ -336,9 +342,13 @@ describe('autoTrackingPlugin', () => { document.getElementById('my-button-id')?.dispatchEvent(new Event('click')); expect(track).toHaveBeenCalledTimes(1); - // trigger click div - document.getElementById('my-div-id')?.dispatchEvent(new Event('click')); + // trigger click div1 + document.getElementById('my-div-id1')?.dispatchEvent(new Event('click')); expect(track).toHaveBeenCalledTimes(2); + + // trigger click div2 + document.getElementById('my-div-id2')?.dispatchEvent(new Event('click')); + expect(track).toHaveBeenCalledTimes(3); }); }); From 0298edd50aca646302dec74a131b33171d9839fb Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Wed, 1 Nov 2023 20:43:48 +0000 Subject: [PATCH 200/214] chore(release): publish - @amplitude/plugin-default-event-tracking-advanced-browser@0.2.0 --- .../CHANGELOG.md | 12 ++++++++++++ .../package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md index 9043bc48c..b58fd5088 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md +++ b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.2.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.1.3...@amplitude/plugin-default-event-tracking-advanced-browser@0.2.0) (2023-11-01) + +### Features + +- add default data attr selector ([#612](https://github.com/amplitude/Amplitude-TypeScript/issues/612)) + ([c0f0c2d](https://github.com/amplitude/Amplitude-TypeScript/commit/c0f0c2dcbcb43959992380fe26b0df961a4ea9f4)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.1.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.1.2...@amplitude/plugin-default-event-tracking-advanced-browser@0.1.3) (2023-10-27) ### Bug Fixes diff --git a/packages/plugin-default-event-tracking-advanced-browser/package.json b/packages/plugin-default-event-tracking-advanced-browser/package.json index 74b47f5f6..5ddaa83eb 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/package.json +++ b/packages/plugin-default-event-tracking-advanced-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-default-event-tracking-advanced-browser", - "version": "0.1.3", + "version": "0.2.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From a7ea04a9c03a0b69a1cab4462d8e1cae2656c79f Mon Sep 17 00:00:00 2001 From: Jesse Wang Date: Wed, 1 Nov 2023 17:36:15 -0700 Subject: [PATCH 201/214] feat(session replay): remove session replay id interface and expose sr properties to plugin --- .../src/session-replay.ts | 6 +- .../test/session-replay.test.ts | 16 +++++ packages/session-replay-browser/src/config.ts | 4 +- .../session-replay-browser/src/helpers.ts | 16 ----- .../src/session-replay.ts | 23 ++----- .../src/typings/session-replay.ts | 2 +- .../test/helpers.test.ts | 16 +---- .../test/session-replay.test.ts | 64 ++----------------- 8 files changed, 38 insertions(+), 109 deletions(-) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index 1a6812ba3..ab76b1c30 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -51,7 +51,7 @@ export class SessionReplayPlugin implements EnrichmentPlugin { async execute(event: Event) { if (event.event_type === DEFAULT_SESSION_START_EVENT && event.session_id) { - sessionReplay.setSessionId({ sessionId: event.session_id }); + sessionReplay.setSessionId(event.session_id); } const sessionRecordingProperties = sessionReplay.getSessionReplayProperties(); @@ -66,6 +66,10 @@ export class SessionReplayPlugin implements EnrichmentPlugin { async teardown(): Promise { sessionReplay.shutdown(); } + + getSessionReplayProperties() { + return sessionReplay.getSessionReplayProperties(); + } } export const sessionReplayPlugin: (options?: SessionReplayOptions) => EnrichmentPlugin = ( diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index f97fa6cf8..31e2bb76c 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -167,4 +167,20 @@ describe('SessionReplayPlugin', () => { expect(shutdown).toHaveBeenCalled(); }); }); + + describe('getSessionReplayProperties', () => { + test('should return session replay properties', async () => { + const sessionReplay = sessionReplayPlugin() as SessionReplayPlugin; + await sessionReplay.setup(mockConfig); + getSessionReplayProperties.mockReturnValueOnce({ + '[Amplitude] Session Recorded': true, + '[Amplitude] Session Replay ID': '123/456', + }); + const sessionReplayProperties = sessionReplay.getSessionReplayProperties(); + expect(sessionReplayProperties).toEqual({ + '[Amplitude] Session Recorded': true, + '[Amplitude] Session Replay ID': '123/456', + }); + }); + }); }); diff --git a/packages/session-replay-browser/src/config.ts b/packages/session-replay-browser/src/config.ts index ccf75c833..acf9f4cef 100644 --- a/packages/session-replay-browser/src/config.ts +++ b/packages/session-replay-browser/src/config.ts @@ -34,9 +34,7 @@ export class SessionReplayConfig extends Config implements ISessionReplayConfig this.apiKey = apiKey; this.sampleRate = options.sampleRate || DEFAULT_SAMPLE_RATE; - if (options.sessionReplayId) { - this.sessionReplayId = options.sessionReplayId; - } else if (options.sessionId && options.deviceId) { + if (options.sessionId && options.deviceId) { this.deviceId = options.deviceId; this.sessionId = options.sessionId; this.sessionReplayId = generateSessionReplayId(options.sessionId, options.deviceId); diff --git a/packages/session-replay-browser/src/helpers.ts b/packages/session-replay-browser/src/helpers.ts index 1295c7928..dfc3fff50 100644 --- a/packages/session-replay-browser/src/helpers.ts +++ b/packages/session-replay-browser/src/helpers.ts @@ -35,19 +35,3 @@ export const getCurrentUrl = () => { export const generateSessionReplayId = (sessionId: number, deviceId: string): string => { return `${deviceId}/${sessionId}`; }; - -export const parseSessionReplayId = ( - sessionReplayId: string | undefined, -): { deviceId?: string; sessionId?: string } => { - if (!sessionReplayId) { - return {}; - } - - const parts = sessionReplayId.split('/'); - if (parts.length === 2) { - const [deviceId, sessionId] = sessionReplayId.split('/'); - return { deviceId, sessionId }; - } - - return {}; -}; diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index 86b0b8cd4..6ea44997b 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -19,13 +19,7 @@ import { STORAGE_PREFIX, defaultSessionStore, } from './constants'; -import { - isSessionInSample, - maskInputFn, - getCurrentUrl, - generateSessionReplayId, - parseSessionReplayId, -} from './helpers'; +import { isSessionInSample, maskInputFn, getCurrentUrl, generateSessionReplayId } from './helpers'; import { MAX_RETRIES_EXCEEDED_MESSAGE, MISSING_API_KEY_MESSAGE, @@ -89,24 +83,21 @@ export class SessionReplay implements AmplitudeSessionReplay { } } - setSessionId({ sessionId, sessionReplayId }: { sessionId?: number; sessionReplayId?: string }) { + setSessionId(sessionId: number) { if (!this.config) { this.loggerProvider.error('Session replay init has not been called, cannot set session id.'); return; } - const lastSessionReplayId = this.config.sessionReplayId; - this.config.sessionId = sessionId; if (sessionId && this.config.deviceId) { this.config.sessionReplayId = generateSessionReplayId(sessionId, this.config.deviceId); - } else if (sessionReplayId) { - this.config.sessionReplayId = sessionReplayId; } else { this.loggerProvider.error('Must provide either session replay id or session id when starting a new session.'); return; } - this.stopRecordingAndSendEvents(lastSessionReplayId); + this.stopRecordingAndSendEvents(this.config.sessionId); + this.config.sessionId = sessionId; this.events = []; this.currentSequenceId = 0; this.recordEvents(); @@ -138,11 +129,12 @@ export class SessionReplay implements AmplitudeSessionReplay { blurListener = () => { this.stopRecordingAndSendEvents(); }; + focusListener = () => { void this.initialize(); }; - stopRecordingAndSendEvents(sessionReplayId?: string) { + stopRecordingAndSendEvents(sessionId?: number) { try { this.stopRecordingEvents && this.stopRecordingEvents(); this.stopRecordingEvents = null; @@ -151,8 +143,7 @@ export class SessionReplay implements AmplitudeSessionReplay { this.loggerProvider.warn(`Error occurred while stopping recording: ${typedError.toString()}`); } - const parseSessionId = parseSessionReplayId(sessionReplayId).sessionId; - const sessionIdToSend = parseSessionId ? +parseSessionId : this.config?.sessionId; + const sessionIdToSend = sessionId || this.config?.sessionId; if (this.events.length && sessionIdToSend) { this.sendEventsList({ events: this.events, diff --git a/packages/session-replay-browser/src/typings/session-replay.ts b/packages/session-replay-browser/src/typings/session-replay.ts index e053a435f..7eea83da4 100644 --- a/packages/session-replay-browser/src/typings/session-replay.ts +++ b/packages/session-replay-browser/src/typings/session-replay.ts @@ -46,7 +46,7 @@ export type SessionReplayOptions = Omit, 'apiKey'>; export interface AmplitudeSessionReplay { init: (apiKey: string, options: SessionReplayOptions) => AmplitudeReturn; - setSessionId: ({ sessionId, sessionReplayId }: { sessionId?: number; sessionReplayId?: string }) => void; + setSessionId: (sessionId: number) => void; getSessionRecordingProperties: () => { [key: string]: boolean | string | null }; getSessionReplayProperties: () => { [key: string]: boolean | string | null }; shutdown: () => void; diff --git a/packages/session-replay-browser/test/helpers.test.ts b/packages/session-replay-browser/test/helpers.test.ts index 6748649be..a51e63faa 100644 --- a/packages/session-replay-browser/test/helpers.test.ts +++ b/packages/session-replay-browser/test/helpers.test.ts @@ -1,5 +1,5 @@ import { UNMASK_TEXT_CLASS } from '../src/constants'; -import { generateHashCode, isSessionInSample, maskInputFn, parseSessionReplayId } from '../src/helpers'; +import { generateHashCode, isSessionInSample, maskInputFn } from '../src/helpers'; describe('SessionReplayPlugin helpers', () => { describe('maskInputFn', () => { @@ -47,18 +47,4 @@ describe('SessionReplayPlugin helpers', () => { expect(result).toEqual(false); }); }); - - describe('parseSessionReplayId', () => { - test('should return empty object if sessionReplayId is not the correct format', () => { - const sessionReplayId = '123/234/345'; - const result = parseSessionReplayId(sessionReplayId); - expect(result).toEqual({}); - }); - - test('should return empty object if sessionReplayId is undefined', () => { - const sessionReplayId = undefined; - const result = parseSessionReplayId(sessionReplayId); - expect(result).toEqual({}); - }); - }); }); diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index 21e901cea..2e7683831 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -98,21 +98,6 @@ describe('SessionReplayPlugin', () => { expect(sessionReplay.storageKey).toBe('AMP_replay_unsent_static_key'); }); - test('should setup plugin with sessionReplayId', async () => { - const sessionReplay = new SessionReplay(); - await sessionReplay.init(apiKey, { ...mockOptions, sampleRate: 0.5, sessionReplayId: '123/456' }).promise; - expect(sessionReplay.config?.transportProvider).toBeDefined(); - expect(sessionReplay.config?.flushMaxRetries).toBe(1); - expect(sessionReplay.config?.optOut).toBe(false); - expect(sessionReplay.config?.sampleRate).toBe(0.5); - expect(sessionReplay.config?.deviceId).toBe(undefined); - expect(sessionReplay.config?.sessionId).toBe(undefined); - expect(sessionReplay.config?.logLevel).toBe(0); - expect(sessionReplay.config?.sessionReplayId).toBe('123/456'); - expect(sessionReplay.loggerProvider).toBeDefined(); - expect(sessionReplay.storageKey).toBe('AMP_replay_unsent_static_key'); - }); - test('should call initalize with shouldSendStoredEvents=true', async () => { const sessionReplay = new SessionReplay(); const initalize = jest.spyOn(sessionReplay, 'initialize').mockReturnValueOnce(Promise.resolve()); @@ -188,7 +173,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.loggerProvider = mockLoggerProvider; const stopRecordingMock = jest.fn(); - sessionReplay.setSessionId({ sessionId: 456 }); + sessionReplay.setSessionId(456); expect(stopRecordingMock).not.toHaveBeenCalled(); // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockLoggerProvider.error).toHaveBeenCalled(); @@ -202,7 +187,7 @@ describe('SessionReplayPlugin', () => { // Mock class as if it has already been recording events sessionReplay.stopRecordingAndSendEvents = stopRecordingMock; - sessionReplay.setSessionId({ sessionId: 456 }); + sessionReplay.setSessionId(456); expect(stopRecordingMock).toHaveBeenCalled(); }); @@ -216,43 +201,20 @@ describe('SessionReplayPlugin', () => { sessionReplay.events = [mockEventString]; sessionReplay.currentSequenceId = 4; - sessionReplay.setSessionId({ sessionId: 456 }); + sessionReplay.setSessionId(456); expect(stopRecordingMock).toHaveBeenCalled(); expect(sessionReplay.config?.sessionId).toEqual(456); expect(sessionReplay.events).toEqual([]); expect(sessionReplay.currentSequenceId).toEqual(0); }); - test('should update the session id with session replay id, reset events and current sequence id, and start recording', async () => { - const sessionReplay = new SessionReplay(); - await sessionReplay.init(apiKey, { - ...mockOptions, - sessionId: undefined, - deviceId: undefined, - sessionReplayId: '123/456', - }).promise; - const stopRecordingMock = jest.fn(); - - // Mock class as if it has already been recording events - sessionReplay.stopRecordingAndSendEvents = stopRecordingMock; - sessionReplay.events = [mockEventString]; - sessionReplay.currentSequenceId = 4; - - sessionReplay.setSessionId({ sessionReplayId: '456/123' }); - expect(stopRecordingMock).toHaveBeenCalled(); - expect(sessionReplay.config?.sessionId).toEqual(undefined); - expect(sessionReplay.config?.deviceId).toEqual(undefined); - expect(sessionReplay.events).toEqual([]); - expect(sessionReplay.currentSequenceId).toEqual(0); - }); - - test('should return early if no session id, device id, and session replay id', async () => { + test('should return early if no session id, device id is set', async () => { const sessionReplay = new SessionReplay(); - await sessionReplay.init(apiKey, mockOptions).promise; + await sessionReplay.init(apiKey, { ...mockOptions, deviceId: undefined }).promise; sessionReplay.loggerProvider = mockLoggerProvider; const stopRecordingMock = jest.fn(); - sessionReplay.setSessionId({}); + sessionReplay.setSessionId(123); expect(stopRecordingMock).not.toHaveBeenCalled(); // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockLoggerProvider.error).toHaveBeenCalled(); @@ -291,18 +253,6 @@ describe('SessionReplayPlugin', () => { }); }); - test('should return session replay id property with session replay id', async () => { - const sessionReplay = new SessionReplay(); - await sessionReplay.init(apiKey, { ...mockOptions, sessionReplayId: '123/456' }).promise; - sessionReplay.getShouldRecord = () => true; - - const result = sessionReplay.getSessionReplayProperties(); - expect(result).toEqual({ - '[Amplitude] Session Recorded': true, - '[Amplitude] Session Replay ID': '123/456', - }); - }); - test('should return session replay id property with null', async () => { const sessionReplay = new SessionReplay(); await sessionReplay.init(apiKey, { ...mockOptions }).promise; @@ -738,7 +688,7 @@ describe('SessionReplayPlugin', () => { sessionReplay.sendEventsList = sendEventsListMock; sessionReplay.events = [mockEventString]; sessionReplay.currentSequenceId = 4; - sessionReplay.stopRecordingAndSendEvents('789/123'); + sessionReplay.stopRecordingAndSendEvents(123); expect(sendEventsListMock).toHaveBeenCalledWith({ events: [mockEventString], sessionId: 123, From b60262785f7adc02eb89e3f7e6df2e31641fe54f Mon Sep 17 00:00:00 2001 From: Jesse Wang Date: Wed, 1 Nov 2023 20:26:42 -0700 Subject: [PATCH 202/214] feat(session replay): error check for session replay id generation --- packages/session-replay-browser/src/config.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/session-replay-browser/src/config.ts b/packages/session-replay-browser/src/config.ts index acf9f4cef..f1863c558 100644 --- a/packages/session-replay-browser/src/config.ts +++ b/packages/session-replay-browser/src/config.ts @@ -33,11 +33,13 @@ export class SessionReplayConfig extends Config implements ISessionReplayConfig this.apiKey = apiKey; this.sampleRate = options.sampleRate || DEFAULT_SAMPLE_RATE; + this.deviceId = options.deviceId; + this.sessionId = options.sessionId; if (options.sessionId && options.deviceId) { - this.deviceId = options.deviceId; - this.sessionId = options.sessionId; this.sessionReplayId = generateSessionReplayId(options.sessionId, options.deviceId); + } else { + this.loggerProvider.error('Please provide both sessionId and deviceId.'); } } } From ace1377ea589fbce177514131396056c9deb738f Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 2 Nov 2023 17:20:51 +0000 Subject: [PATCH 203/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.7.0 - @amplitude/session-replay-browser@0.4.0 --- .../CHANGELOG.md | 14 +++++++++++ .../package.json | 4 ++-- packages/session-replay-browser/CHANGELOG.md | 23 +++++++++++++++++++ packages/session-replay-browser/package.json | 2 +- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index b32fb672f..a15b3ce7b 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.7.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.16...@amplitude/plugin-session-replay-browser@0.7.0) (2023-11-02) + +### Features + +- **session replay:** parse sessionReplayId for sessionId and deviceId + ([990ddaf](https://github.com/amplitude/Amplitude-TypeScript/commit/990ddaf102465e52d84e496d280c8aada5f94944)) +- **session replay:** remove session replay id interface and expose sr properties to plugin + ([a7ea04a](https://github.com/amplitude/Amplitude-TypeScript/commit/a7ea04a9c03a0b69a1cab4462d8e1cae2656c79f)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.6.16](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.15...@amplitude/plugin-session-replay-browser@0.6.16) (2023-10-26) ### Bug Fixes diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 348ccb8b5..27e7fff65 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.6.16", + "version": "0.7.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -40,7 +40,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "^0.3.1", + "@amplitude/session-replay-browser": "^0.4.0", "idb-keyval": "^6.2.1", "tslib": "^2.4.1" }, diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index e58ac0629..eec8ba7e4 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,29 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.4.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.3.1...@amplitude/session-replay-browser@0.4.0) (2023-11-02) + +### Bug Fixes + +- **session replay:** testing changes for sessionReplayId + ([60f6b04](https://github.com/amplitude/Amplitude-TypeScript/commit/60f6b04bbe5069f97279c67098ef6a8df259b678)) + +### Features + +- add session replay ID generic identifier to init/turnover + ([03a67c9](https://github.com/amplitude/Amplitude-TypeScript/commit/03a67c97e6c5589662be0a1a7e4cd7a725774d85)) +- **session replay:** error check for session replay id generation + ([b602627](https://github.com/amplitude/Amplitude-TypeScript/commit/b60262785f7adc02eb89e3f7e6df2e31641fe54f)) +- **session replay:** parse sessionReplayId for sessionId and deviceId + ([990ddaf](https://github.com/amplitude/Amplitude-TypeScript/commit/990ddaf102465e52d84e496d280c8aada5f94944)) +- **session replay:** remove session replay id interface and expose sr properties to plugin + ([a7ea04a](https://github.com/amplitude/Amplitude-TypeScript/commit/a7ea04a9c03a0b69a1cab4462d8e1cae2656c79f)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.3.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.3.0...@amplitude/session-replay-browser@0.3.1) (2023-10-26) ### Bug Fixes diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index 6ce63ebb6..8e27a2b55 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "0.3.1", + "version": "0.4.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 83d156a4d2bf077fa023faad0ef514a4dd074b6f Mon Sep 17 00:00:00 2001 From: Jesse Wang Date: Thu, 2 Nov 2023 13:52:48 -0700 Subject: [PATCH 204/214] feat(session replay): remove recording terms --- .../session-replay-browser/src/constants.ts | 3 +- packages/session-replay-browser/src/index.ts | 3 +- .../src/session-replay-factory.ts | 5 --- .../src/session-replay.ts | 10 +---- .../src/typings/session-replay.ts | 1 - .../session-replay-browser/test/index.test.ts | 4 +- .../test/session-replay.test.ts | 44 +------------------ 7 files changed, 6 insertions(+), 64 deletions(-) diff --git a/packages/session-replay-browser/src/constants.ts b/packages/session-replay-browser/src/constants.ts index d55122d65..e7e1444d9 100644 --- a/packages/session-replay-browser/src/constants.ts +++ b/packages/session-replay-browser/src/constants.ts @@ -3,8 +3,7 @@ import { IDBStoreSession } from './typings/session-replay'; export const DEFAULT_EVENT_PROPERTY_PREFIX = '[Amplitude]'; -export const NEW_SESSION_REPLAY_PROPERTY = `${DEFAULT_EVENT_PROPERTY_PREFIX} Session Replay ID`; -export const DEFAULT_SESSION_REPLAY_PROPERTY = `${DEFAULT_EVENT_PROPERTY_PREFIX} Session Recorded`; +export const DEFAULT_SESSION_REPLAY_PROPERTY = `${DEFAULT_EVENT_PROPERTY_PREFIX} Session Replay ID`; export const DEFAULT_SESSION_START_EVENT = 'session_start'; export const DEFAULT_SESSION_END_EVENT = 'session_end'; export const DEFAULT_SAMPLE_RATE = 0; diff --git a/packages/session-replay-browser/src/index.ts b/packages/session-replay-browser/src/index.ts index 0cd9bdbf7..b59f8db65 100644 --- a/packages/session-replay-browser/src/index.ts +++ b/packages/session-replay-browser/src/index.ts @@ -1,3 +1,2 @@ import sessionReplay from './session-replay-factory'; -export const { init, setSessionId, getSessionRecordingProperties, getSessionReplayProperties, shutdown } = - sessionReplay; +export const { init, setSessionId, getSessionReplayProperties, shutdown } = sessionReplay; diff --git a/packages/session-replay-browser/src/session-replay-factory.ts b/packages/session-replay-browser/src/session-replay-factory.ts index b7b57db5a..64b871066 100644 --- a/packages/session-replay-browser/src/session-replay-factory.ts +++ b/packages/session-replay-browser/src/session-replay-factory.ts @@ -22,11 +22,6 @@ const createInstance: () => AmplitudeSessionReplay = () => { 'setSessionId', getLogConfig(sessionReplay), ), - getSessionRecordingProperties: debugWrapper( - sessionReplay.getSessionRecordingProperties.bind(sessionReplay), - 'getSessionRecordingProperties', - getLogConfig(sessionReplay), - ), getSessionReplayProperties: debugWrapper( sessionReplay.getSessionReplayProperties.bind(sessionReplay), 'getSessionReplayProperties', diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index 6ea44997b..36ab4281c 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -13,7 +13,6 @@ import { MAX_IDB_STORAGE_LENGTH, MAX_INTERVAL, MIN_INTERVAL, - NEW_SESSION_REPLAY_PROPERTY, SESSION_REPLAY_EU_URL as SESSION_REPLAY_EU_SERVER_URL, SESSION_REPLAY_SERVER_URL, STORAGE_PREFIX, @@ -112,20 +111,13 @@ export class SessionReplay implements AmplitudeSessionReplay { if (shouldRecord) { return { - [DEFAULT_SESSION_REPLAY_PROPERTY]: true, - [NEW_SESSION_REPLAY_PROPERTY]: this.config.sessionReplayId ? this.config.sessionReplayId : null, + [DEFAULT_SESSION_REPLAY_PROPERTY]: this.config.sessionReplayId ? this.config.sessionReplayId : null, }; } return {}; } - getSessionRecordingProperties = () => { - this.loggerProvider.warn('Please use getSessionReplayProperties instead of getSessionRecordingProperties.'); - - return this.getSessionReplayProperties(); - }; - blurListener = () => { this.stopRecordingAndSendEvents(); }; diff --git a/packages/session-replay-browser/src/typings/session-replay.ts b/packages/session-replay-browser/src/typings/session-replay.ts index 7eea83da4..f36736c9c 100644 --- a/packages/session-replay-browser/src/typings/session-replay.ts +++ b/packages/session-replay-browser/src/typings/session-replay.ts @@ -47,7 +47,6 @@ export type SessionReplayOptions = Omit, 'apiKey'>; export interface AmplitudeSessionReplay { init: (apiKey: string, options: SessionReplayOptions) => AmplitudeReturn; setSessionId: (sessionId: number) => void; - getSessionRecordingProperties: () => { [key: string]: boolean | string | null }; getSessionReplayProperties: () => { [key: string]: boolean | string | null }; shutdown: () => void; } diff --git a/packages/session-replay-browser/test/index.test.ts b/packages/session-replay-browser/test/index.test.ts index b14e3ad65..a1630a2e8 100644 --- a/packages/session-replay-browser/test/index.test.ts +++ b/packages/session-replay-browser/test/index.test.ts @@ -1,10 +1,10 @@ -import { getSessionRecordingProperties, init, setSessionId, shutdown } from '../src/index'; +import { getSessionReplayProperties, init, setSessionId, shutdown } from '../src/index'; describe('index', () => { test('should expose apis', () => { expect(typeof init).toBe('function'); expect(typeof setSessionId).toBe('function'); - expect(typeof getSessionRecordingProperties).toBe('function'); + expect(typeof getSessionReplayProperties).toBe('function'); expect(typeof shutdown).toBe('function'); }); }); diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index 2e7683831..d618bb07b 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -248,7 +248,6 @@ describe('SessionReplayPlugin', () => { const result = sessionReplay.getSessionReplayProperties(); expect(result).toEqual({ - '[Amplitude] Session Recorded': true, '[Amplitude] Session Replay ID': '1a2b3c/123', }); }); @@ -263,52 +262,11 @@ describe('SessionReplayPlugin', () => { const result = sessionReplay.getSessionReplayProperties(); expect(result).toEqual({ - '[Amplitude] Session Recorded': true, '[Amplitude] Session Replay ID': null, }); }); }); - describe('getSessionRecordingProperties', () => { - test('should return an empty object if config not set', () => { - const sessionReplay = new SessionReplay(); - sessionReplay.loggerProvider = mockLoggerProvider; - - const result = sessionReplay.getSessionRecordingProperties(); - expect(result).toEqual({}); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(mockLoggerProvider.error).toHaveBeenCalled(); - }); - - test('should return an empty object if shouldRecord is false', async () => { - const sessionReplay = new SessionReplay(); - await sessionReplay.init(apiKey, mockOptions).promise; - sessionReplay.getShouldRecord = () => false; - const result = sessionReplay.getSessionRecordingProperties(); - expect(result).toEqual({}); - }); - - test('should return an default sample rate if not set', async () => { - const sessionReplay = new SessionReplay(); - await sessionReplay.init(apiKey, mockOptions).promise; - sessionReplay.getShouldRecord = () => false; - const result = sessionReplay.getSessionRecordingProperties(); - expect(result).toEqual({}); - }); - - test('should return the session recorded property if shouldRecord is true', async () => { - const sessionReplay = new SessionReplay(); - await sessionReplay.init(apiKey, mockOptions).promise; - sessionReplay.getShouldRecord = () => true; - - const result = sessionReplay.getSessionRecordingProperties(); - expect(result).toEqual({ - '[Amplitude] Session Recorded': true, - '[Amplitude] Session Replay ID': '1a2b3c/123', - }); - }); - }); - describe('initalize', () => { test('should read events from storage and send them if shouldSendStoredEvents is true', async () => { const sessionReplay = new SessionReplay(); @@ -1261,7 +1219,7 @@ describe('SessionReplayPlugin', () => { await sessionReplay.init(apiKey, { ...mockOptions, sampleRate: 0.8 }).promise; const sessionRecordingProperties = sessionReplay.getSessionReplayProperties(); expect(sessionRecordingProperties).toMatchObject({ - [DEFAULT_SESSION_REPLAY_PROPERTY]: true, + [DEFAULT_SESSION_REPLAY_PROPERTY]: '1a2b3c/123', }); // Log is called from setup, but that's not what we're testing here mockLoggerProvider.log.mockClear(); From cd378b36200345721bc69a95c5d6daf7cf8b52cd Mon Sep 17 00:00:00 2001 From: Marvin Liu Date: Thu, 2 Nov 2023 17:56:38 -0700 Subject: [PATCH 205/214] fix: skip parent element event when it is not target (#613) * fix: skip parent element event when it is not target * feat: limit to only the innermost allowed element --- .../default-event-tracking-advanced-plugin.ts | 21 +- .../src/helpers.ts | 13 ++ .../default-event-tracking-advanced.test.ts | 219 ++++++++++++++++++ .../test/helpers.test.ts | 57 +++++ 4 files changed, 308 insertions(+), 2 deletions(-) diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts index dc41bad4d..25bce21ca 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts @@ -8,6 +8,7 @@ import { removeEmptyProperties, getNearestLabel, querySelectUniqueElements, + getClosestElement, } from './helpers'; import { finder } from './libs/finder'; @@ -200,13 +201,29 @@ export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): Brows } const addListener = (el: Element) => { if (shouldTrackEvent('click', el)) { - addEventListener(el, 'click', () => { + addEventListener(el, 'click', (event: Event) => { + // Limit to only the innermost element that matches the selectors, avoiding all propagated event after matching. + /* istanbul ignore next */ + if ( + event?.target != event?.currentTarget && + getClosestElement(event?.target as HTMLElement, cssSelectorAllowlist) != event?.currentTarget + ) { + return; + } /* istanbul ignore next */ amplitude?.track(constants.AMPLITUDE_ELEMENT_CLICKED_EVENT, getEventProperties('click', el)); }); } if (shouldTrackEvent('change', el)) { - addEventListener(el, 'change', () => { + addEventListener(el, 'change', (event: Event) => { + // Limit to only the innermost element that matches the selectors, avoiding all propagated event after matching. + /* istanbul ignore next */ + if ( + event?.target != event?.currentTarget && + getClosestElement(event?.target as HTMLElement, cssSelectorAllowlist) != event?.currentTarget + ) { + return; + } /* istanbul ignore next */ amplitude?.track(constants.AMPLITUDE_ELEMENT_CHANGED_EVENT, getEventProperties('change', el)); }); diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts b/packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts index fa82a17dc..bb16a27f4 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts @@ -123,3 +123,16 @@ export const querySelectUniqueElements = (root: Element | Document, selectors: s }, new Set()); return Array.from(elementSet); }; + +// Similar as element.closest, but works with multiple selectors +export const getClosestElement = (element: Element | null, selectors: string[]): Element | null => { + if (!element) { + return null; + } + /* istanbul ignore next */ + if (selectors.some((selector) => element?.matches?.(selector))) { + return element; + } + /* istanbul ignore next */ + return getClosestElement(element?.parentElement, selectors); +}; diff --git a/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts b/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts index 8c7349fc0..c3e104d61 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts @@ -598,6 +598,225 @@ describe('autoTrackingPlugin', () => { document.getElementById('my-div-id')?.dispatchEvent(new Event('click')); expect(track).toHaveBeenCalledTimes(0); }); + + describe('when facing nested elements', () => { + /* +
+
+
+ click me +
+
+
+ cssSelectorAllowlist: ['div'] + expect: only track inner, as we should only track the innermost allowed element + */ + test('should only fire event for the inner element when container element also matches the allowlist and is the same tag', async () => { + document.getElementsByTagName('body')[0].innerHTML = ` +
+
+
+ click me +
+
+
+ `; + + plugin = defaultEventTrackingAdvancedPlugin({ cssSelectorAllowlist: ['div'] }); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click inner + document.getElementById('inner')?.dispatchEvent(new Event('click', { bubbles: true })); + expect(track).toHaveBeenCalledTimes(1); + expect(track).toHaveBeenNthCalledWith( + 1, + '[Amplitude] Element Clicked', + expect.objectContaining({ + '[Amplitude] Element ID': 'inner', + }), + ); + + // trigger click container + document.getElementById('container1')?.dispatchEvent(new Event('click', { bubbles: true })); + expect(track).toHaveBeenCalledTimes(2); + expect(track).toHaveBeenNthCalledWith( + 2, + '[Amplitude] Element Clicked', + expect.objectContaining({ + '[Amplitude] Element ID': 'container1', + }), + ); + }); + + /* +
+
+
+ click me +
+
+
+ cssSelectorAllowlist: ['.match-me'] + expect: only track container1, as we should only track the innermost allowed element + */ + test('should only fire event for the immediate parent element when inner element does not match but parent matches', async () => { + document.getElementsByTagName('body')[0].innerHTML = ` +
+
+
+ click me +
+
+
+ `; + + plugin = defaultEventTrackingAdvancedPlugin({ cssSelectorAllowlist: ['.match-me'] }); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click inner + document.getElementById('inner')?.dispatchEvent(new Event('click', { bubbles: true })); + expect(track).toHaveBeenCalledTimes(1); + expect(track).toHaveBeenNthCalledWith( + 1, + '[Amplitude] Element Clicked', + expect.objectContaining({ + '[Amplitude] Element ID': 'container1', + }), + ); + + // trigger click container + document.getElementById('container1')?.dispatchEvent(new Event('click', { bubbles: true })); + expect(track).toHaveBeenCalledTimes(2); + expect(track).toHaveBeenNthCalledWith( + 2, + '[Amplitude] Element Clicked', + expect.objectContaining({ + '[Amplitude] Element ID': 'container1', + }), + ); + }); + + /* + + cssSelectorAllowlist: ['button'] + expect: only track button click, as div is not allowed + */ + test('should only fire event for the container element when inner element does not match the allowlist', async () => { + document.getElementsByTagName('body')[0].innerHTML = ` + + `; + + plugin = defaultEventTrackingAdvancedPlugin({ cssSelectorAllowlist: ['button'] }); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click inner + document.getElementById('inner')?.dispatchEvent(new Event('click', { bubbles: true })); + expect(track).toHaveBeenCalledTimes(1); + expect(track).toHaveBeenNthCalledWith( + 1, + '[Amplitude] Element Clicked', + expect.objectContaining({ + '[Amplitude] Element ID': 'container', + }), + ); + + // trigger click container + document.getElementById('container')?.dispatchEvent(new Event('click', { bubbles: true })); + expect(track).toHaveBeenCalledTimes(2); + expect(track).toHaveBeenNthCalledWith( + 2, + '[Amplitude] Element Clicked', + expect.objectContaining({ + '[Amplitude] Element ID': 'container', + }), + ); + }); + + /* + + cssSelectorAllowlist: ['[data-track]'] + expect: only track div click, as div is innermost element that matches allowlist + note: we do not track the button click here, this is a rare case that the inner div is also allowed + */ + test('should only fire event for the inner element when container element also matches the allowlist and is different tag', async () => { + document.getElementsByTagName('body')[0].innerHTML = ` + + `; + + plugin = defaultEventTrackingAdvancedPlugin({ cssSelectorAllowlist: ['[data-track]'] }); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + await plugin?.setup(config as BrowserConfig, instance); + + // trigger click inner + document.getElementById('inner')?.dispatchEvent(new Event('click', { bubbles: true })); + expect(track).toHaveBeenCalledTimes(1); + expect(track).toHaveBeenNthCalledWith( + 1, + '[Amplitude] Element Clicked', + expect.objectContaining({ + '[Amplitude] Element ID': 'inner', + }), + ); + + // trigger click container + document.getElementById('container')?.dispatchEvent(new Event('click', { bubbles: true })); + expect(track).toHaveBeenCalledTimes(2); + expect(track).toHaveBeenNthCalledWith( + 2, + '[Amplitude] Element Clicked', + expect.objectContaining({ + '[Amplitude] Element ID': 'container', + }), + ); + }); + }); }); describe('teardown', () => { diff --git a/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts b/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts index b7f15d745..9f0916817 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts @@ -9,9 +9,15 @@ import { removeEmptyProperties, getNearestLabel, querySelectUniqueElements, + getClosestElement, } from '../src/helpers'; describe('default-event-tracking-advanced-plugin helpers', () => { + afterEach(() => { + document.getElementsByTagName('body')[0].innerHTML = ''; + jest.clearAllMocks(); + }); + describe('isNonSensitiveString', () => { test('should return false when text is missing', () => { const text = null; @@ -336,4 +342,55 @@ describe('default-event-tracking-advanced-plugin helpers', () => { expect(result).toEqual([div1, div2]); }); }); + + describe('getClosestElement', () => { + test('should return null when element null', () => { + expect(getClosestElement(null, ['div'])).toEqual(null); + }); + + test('should return current element if it matches any selectors', () => { + document.getElementsByTagName('body')[0].innerHTML = ` +
+
+ xxx +
+
+ `; + + const inner = document.getElementById('inner'); + expect(getClosestElement(inner, ['span', 'div'])?.id).toEqual('inner'); + }); + + test('should return closest element if it matches any selectors', () => { + document.getElementsByTagName('body')[0].innerHTML = ` +
+
+
+
+
+ xxx +
+
+
+ `; + + const inner = document.getElementById('inner'); + expect(getClosestElement(inner, ['span', '[data-target]'])?.id).toEqual('parent2'); + }); + + test('should return null when no element matches', () => { + document.getElementsByTagName('body')[0].innerHTML = ` +
+
+
+ xxx +
+
+
+ `; + + const inner = document.getElementById('inner'); + expect(getClosestElement(inner, ['div.some-class'])).toEqual(null); + }); + }); }); From c636eea938ba2830e741e5d67fd53c38f2e548d1 Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Fri, 3 Nov 2023 01:05:07 +0000 Subject: [PATCH 206/214] chore(release): publish - @amplitude/plugin-default-event-tracking-advanced-browser@0.2.1 --- .../CHANGELOG.md | 12 ++++++++++++ .../package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md index b58fd5088..cd54371c0 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md +++ b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.2.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.2.0...@amplitude/plugin-default-event-tracking-advanced-browser@0.2.1) (2023-11-03) + +### Bug Fixes + +- skip parent element event when it is not target ([#613](https://github.com/amplitude/Amplitude-TypeScript/issues/613)) + ([cd378b3](https://github.com/amplitude/Amplitude-TypeScript/commit/cd378b36200345721bc69a95c5d6daf7cf8b52cd)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [0.2.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.1.3...@amplitude/plugin-default-event-tracking-advanced-browser@0.2.0) (2023-11-01) ### Features diff --git a/packages/plugin-default-event-tracking-advanced-browser/package.json b/packages/plugin-default-event-tracking-advanced-browser/package.json index 5ddaa83eb..dac18ad16 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/package.json +++ b/packages/plugin-default-event-tracking-advanced-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-default-event-tracking-advanced-browser", - "version": "0.2.0", + "version": "0.2.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 151a509e1aa6a6bd7f2d4812448a7462ea0abadc Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Fri, 10 Nov 2023 01:02:54 +0000 Subject: [PATCH 207/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.7.1-beta.0 - @amplitude/session-replay-browser@0.5.0-beta.0 --- packages/plugin-session-replay-browser/CHANGELOG.md | 9 +++++++++ packages/plugin-session-replay-browser/package.json | 4 ++-- packages/session-replay-browser/CHANGELOG.md | 12 ++++++++++++ packages/session-replay-browser/package.json | 2 +- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index a15b3ce7b..86bd63972 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.7.1-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.7.0...@amplitude/plugin-session-replay-browser@0.7.1-beta.0) (2023-11-10) + +**Note:** Version bump only for package @amplitude/plugin-session-replay-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [0.7.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.6.16...@amplitude/plugin-session-replay-browser@0.7.0) (2023-11-02) ### Features diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 27e7fff65..dd9555785 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.7.0", + "version": "0.7.1-beta.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -40,7 +40,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "^0.4.0", + "@amplitude/session-replay-browser": "^0.5.0-beta.0", "idb-keyval": "^6.2.1", "tslib": "^2.4.1" }, diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index eec8ba7e4..11b6268c0 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.5.0-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.4.0...@amplitude/session-replay-browser@0.5.0-beta.0) (2023-11-10) + +### Features + +- **session replay:** remove recording terms + ([83d156a](https://github.com/amplitude/Amplitude-TypeScript/commit/83d156a4d2bf077fa023faad0ef514a4dd074b6f)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [0.4.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.3.1...@amplitude/session-replay-browser@0.4.0) (2023-11-02) ### Bug Fixes diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index 8e27a2b55..e642c4203 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "0.4.0", + "version": "0.5.0-beta.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 16f7655ce30f10de781968208908c08e7e8ec21c Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 16 Nov 2023 18:16:34 +0000 Subject: [PATCH 208/214] chore(release): publish - @amplitude/plugin-session-replay-browser@0.7.1 - @amplitude/session-replay-browser@0.5.0 --- packages/plugin-session-replay-browser/CHANGELOG.md | 9 +++++++++ packages/plugin-session-replay-browser/package.json | 4 ++-- packages/session-replay-browser/CHANGELOG.md | 9 +++++++++ packages/session-replay-browser/package.json | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 86bd63972..372842375 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.7.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.7.1-beta.0...@amplitude/plugin-session-replay-browser@0.7.1) (2023-11-16) + +**Note:** Version bump only for package @amplitude/plugin-session-replay-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.7.1-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.7.0...@amplitude/plugin-session-replay-browser@0.7.1-beta.0) (2023-11-10) **Note:** Version bump only for package @amplitude/plugin-session-replay-browser diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index dd9555785..5dba70202 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.7.1-beta.0", + "version": "0.7.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", @@ -40,7 +40,7 @@ "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", - "@amplitude/session-replay-browser": "^0.5.0-beta.0", + "@amplitude/session-replay-browser": "^0.5.0", "idb-keyval": "^6.2.1", "tslib": "^2.4.1" }, diff --git a/packages/session-replay-browser/CHANGELOG.md b/packages/session-replay-browser/CHANGELOG.md index 11b6268c0..5054b734f 100644 --- a/packages/session-replay-browser/CHANGELOG.md +++ b/packages/session-replay-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.5.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.5.0-beta.0...@amplitude/session-replay-browser@0.5.0) (2023-11-16) + +**Note:** Version bump only for package @amplitude/session-replay-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + # [0.5.0-beta.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/session-replay-browser@0.4.0...@amplitude/session-replay-browser@0.5.0-beta.0) (2023-11-10) ### Features diff --git a/packages/session-replay-browser/package.json b/packages/session-replay-browser/package.json index e642c4203..70f737758 100644 --- a/packages/session-replay-browser/package.json +++ b/packages/session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/session-replay-browser", - "version": "0.5.0-beta.0", + "version": "0.5.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 7afb5c72aba497b7feba980fdf49f061a1009de2 Mon Sep 17 00:00:00 2001 From: Marvin Liu Date: Thu, 16 Nov 2023 12:43:46 -0800 Subject: [PATCH 209/214] feat: integrate with selector (#620) * feat: integrate with selector * refactor: better naming and using constant --- .../src/constants.ts | 4 + .../default-event-tracking-advanced-plugin.ts | 16 ++++ .../src/helpers.ts | 29 +++++++ .../src/libs/messenger.ts | 81 +++++++++++++++++++ .../default-event-tracking-advanced.test.ts | 24 ++++++ .../test/helpers.test.ts | 31 +++++++ 6 files changed, 185 insertions(+) create mode 100644 packages/plugin-default-event-tracking-advanced-browser/src/libs/messenger.ts diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/constants.ts b/packages/plugin-default-event-tracking-advanced-browser/src/constants.ts index 6adf61369..14c97510d 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/src/constants.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/constants.ts @@ -18,3 +18,7 @@ export const AMPLITUDE_EVENT_PROP_PAGE_URL = '[Amplitude] Page URL'; export const AMPLITUDE_EVENT_PROP_PAGE_TITLE = '[Amplitude] Page Title'; export const AMPLITUDE_EVENT_PROP_VIEWPORT_HEIGHT = '[Amplitude] Viewport Height'; export const AMPLITUDE_EVENT_PROP_VIEWPORT_WIDTH = '[Amplitude] Viewport Width'; + +export const AMPLITUDE_ORIGIN = 'https://app.amplitude.com'; +export const AMPLITUDE_VISUAL_TAGGING_SELECTOR_SCRIPT_URL = + 'https://cdn.amplitude.com/libs/visual-tagging-selector-0.0.0.js.gz'; diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts index 25bce21ca..fb56e7486 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/default-event-tracking-advanced-plugin.ts @@ -11,6 +11,7 @@ import { getClosestElement, } from './helpers'; import { finder } from './libs/finder'; +import { Messenger, WindowMessenger } from './libs/messenger'; type BrowserEnrichmentPlugin = EnrichmentPlugin; type ActionType = 'click' | 'change'; @@ -63,6 +64,11 @@ interface Options { * Default is 'data-amp-auto-track-'. */ dataAttributePrefix?: string; + + visualTaggingOptions?: { + enabled?: boolean; + messenger?: Messenger; + }; } export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): BrowserEnrichmentPlugin => { @@ -71,6 +77,10 @@ export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): Brows pageUrlAllowlist, shouldTrackEventResolver, dataAttributePrefix = DEFAULT_DATA_ATTRIBUTE_PREFIX, + visualTaggingOptions = { + enabled: true, + messenger: new WindowMessenger(), + }, } = options; const name = constants.PLUGIN_NAME; const type = 'enrichment'; @@ -249,6 +259,12 @@ export const defaultEventTrackingAdvancedPlugin = (options: Options = {}): Brows } /* istanbul ignore next */ config?.loggerProvider?.log(`${name} has been successfully added.`); + + // Setup visual tagging selector + if (window.opener && visualTaggingOptions.enabled) { + /* istanbul ignore next */ + visualTaggingOptions.messenger?.setup(); + } }; const execute: BrowserEnrichmentPlugin['execute'] = async (event) => { diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts b/packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts index bb16a27f4..a2bc5164d 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/src/helpers.ts @@ -136,3 +136,32 @@ export const getClosestElement = (element: Element | null, selectors: string[]): /* istanbul ignore next */ return getClosestElement(element?.parentElement, selectors); }; + +export const asyncLoadScript = (url: string) => { + return new Promise((resolve, reject) => { + try { + const scriptElement = document.createElement('script'); + scriptElement.type = 'text/javascript'; + scriptElement.async = true; + scriptElement.src = url; + scriptElement.addEventListener( + 'load', + () => { + resolve({ status: true }); + }, + { once: true }, + ); + scriptElement.addEventListener('error', () => { + reject({ + status: false, + message: `Failed to load the script ${url}`, + }); + }); + /* istanbul ignore next */ + document.head?.appendChild(scriptElement); + } catch (error) { + /* istanbul ignore next */ + reject(error); + } + }); +}; diff --git a/packages/plugin-default-event-tracking-advanced-browser/src/libs/messenger.ts b/packages/plugin-default-event-tracking-advanced-browser/src/libs/messenger.ts new file mode 100644 index 000000000..18a13fb42 --- /dev/null +++ b/packages/plugin-default-event-tracking-advanced-browser/src/libs/messenger.ts @@ -0,0 +1,81 @@ +/* istanbul ignore file */ +/* eslint-disable no-restricted-globals */ +import { AMPLITUDE_ORIGIN, AMPLITUDE_VISUAL_TAGGING_SELECTOR_SCRIPT_URL } from '../constants'; +import { asyncLoadScript } from '../helpers'; +import { Logger } from '@amplitude/analytics-types'; + +export interface Messenger { + logger?: Logger; + setup: () => void; +} + +interface Data { + [key: string]: any; +} + +interface Message { + action: string; + data?: Data; +} + +export const Action = { + Ping: 'ping', + Pong: 'pong', + PageLoaded: 'page-loaded', + SelectorLoaded: 'selector-loaded', + InitializeVisualTaggingSelector: 'initialize-visual-tagging-selector', + CloseVisualTaggingSelector: 'close-visual-tagging-selector', + ElementSelected: 'element-selected', +}; + +export class WindowMessenger implements Messenger { + endpoint = AMPLITUDE_ORIGIN; + logger?: Logger; + + constructor({ logger }: { logger?: Logger } = {}) { + this.logger = logger; + } + + private notify(message: Message) { + this.logger?.debug('Message sent: ', message); + (window.opener as WindowProxy)?.postMessage?.(message, this.endpoint); + } + + setup() { + let amplitudeVisualTaggingSelectorInstance: any = null; + window.addEventListener('message', (event) => { + this.logger?.debug('Message received: ', event); + if (this.endpoint !== event.origin) { + return; + } + const eventData = event?.data as Message; + const action = eventData?.action; + if (!action) { + return; + } + if (action === Action.Ping) { + this.notify({ action: Action.Pong }); + } else if (action === Action.InitializeVisualTaggingSelector) { + asyncLoadScript(AMPLITUDE_VISUAL_TAGGING_SELECTOR_SCRIPT_URL) + .then(() => { + // eslint-disable-next-line + amplitudeVisualTaggingSelectorInstance = (window as any)?.amplitudeVisualTaggingSelector?.({ + onSelect: this.onSelect, + }); + this.notify({ action: Action.SelectorLoaded }); + }) + .catch(() => { + this.logger?.warn('Failed to initialize visual tagging selector'); + }); + } else if (action === Action.CloseVisualTaggingSelector) { + // eslint-disable-next-line + amplitudeVisualTaggingSelectorInstance?.close?.(); + } + }); + this.notify({ action: Action.PageLoaded }); + } + + private onSelect = (data: Data) => { + this.notify({ action: Action.ElementSelected, data }); + }; +} diff --git a/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts b/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts index c3e104d61..3f8be37de 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/test/default-event-tracking-advanced.test.ts @@ -87,6 +87,30 @@ describe('autoTrackingPlugin', () => { } plugin requires a later version of @amplitude/analytics-browser. Events are not tracked.`, ); }); + + test('should setup visual tagging selector', async () => { + window.opener = true; + const messengerMock = { + setup: jest.fn(), + }; + plugin = defaultEventTrackingAdvancedPlugin({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + visualTaggingOptions: { enabled: true, messenger: messengerMock as any }, + }); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as Logger, + }; + const amplitude: Partial = {}; + await plugin?.setup?.(config as BrowserConfig, amplitude as BrowserClient); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect((messengerMock as any).setup).toHaveBeenCalledTimes(1); + }); }); describe('execute', () => { diff --git a/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts b/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts index 9f0916817..2f93e1367 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts +++ b/packages/plugin-default-event-tracking-advanced-browser/test/helpers.test.ts @@ -10,6 +10,7 @@ import { getNearestLabel, querySelectUniqueElements, getClosestElement, + asyncLoadScript, } from '../src/helpers'; describe('default-event-tracking-advanced-plugin helpers', () => { @@ -393,4 +394,34 @@ describe('default-event-tracking-advanced-plugin helpers', () => { expect(getClosestElement(inner, ['div.some-class'])).toEqual(null); }); }); + + describe('asyncLoadScript', () => { + test('should append the script to document and resolve with status true', () => { + void asyncLoadScript('https://test-url.amplitude/').then((result) => { + expect(result).toEqual({ status: true }); + }); + const script = document.getElementsByTagName('script')[0]; + expect(document.getElementsByTagName('script')[0].src).toEqual('https://test-url.amplitude/'); + + script.dispatchEvent(new Event('load')); + }); + + test('should reject with status false when error', () => { + void asyncLoadScript('https://test-url.amplitude/').then( + () => { + expect('should not be called').toEqual(true); + }, + (result) => { + expect(result).toEqual({ + status: false, + message: 'Failed to load the script https://test-url.amplitude/', + }); + }, + ); + const script = document.getElementsByTagName('script')[0]; + expect(document.getElementsByTagName('script')[0].src).toEqual('https://test-url.amplitude/'); + + script.dispatchEvent(new Event('error')); + }); + }); }); From 004e0da033ffa433505812100bdb5cfb729980ae Mon Sep 17 00:00:00 2001 From: Jesse Wang Date: Thu, 16 Nov 2023 13:23:41 -0800 Subject: [PATCH 210/214] chore(session replay): manually bump version to 0.8.0 --- packages/plugin-session-replay-browser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index 5dba70202..ac5de50c5 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.7.1", + "version": "0.8.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 3955739a7a0631883c03cd7150e467a4feed0dfd Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 16 Nov 2023 23:08:29 +0000 Subject: [PATCH 211/214] chore(release): publish - @amplitude/plugin-default-event-tracking-advanced-browser@0.3.0 - @amplitude/plugin-session-replay-browser@0.8.1 --- .../CHANGELOG.md | 12 ++++++++++++ .../package.json | 2 +- packages/plugin-session-replay-browser/CHANGELOG.md | 9 +++++++++ packages/plugin-session-replay-browser/package.json | 2 +- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md index cd54371c0..1ff9c1967 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md +++ b/packages/plugin-default-event-tracking-advanced-browser/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.3.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.2.1...@amplitude/plugin-default-event-tracking-advanced-browser@0.3.0) (2023-11-16) + +### Features + +- integrate with selector ([#620](https://github.com/amplitude/Amplitude-TypeScript/issues/620)) + ([7afb5c7](https://github.com/amplitude/Amplitude-TypeScript/commit/7afb5c72aba497b7feba980fdf49f061a1009de2)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.2.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-default-event-tracking-advanced-browser@0.2.0...@amplitude/plugin-default-event-tracking-advanced-browser@0.2.1) (2023-11-03) ### Bug Fixes diff --git a/packages/plugin-default-event-tracking-advanced-browser/package.json b/packages/plugin-default-event-tracking-advanced-browser/package.json index dac18ad16..8990f5834 100644 --- a/packages/plugin-default-event-tracking-advanced-browser/package.json +++ b/packages/plugin-default-event-tracking-advanced-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-default-event-tracking-advanced-browser", - "version": "0.2.1", + "version": "0.3.0", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", diff --git a/packages/plugin-session-replay-browser/CHANGELOG.md b/packages/plugin-session-replay-browser/CHANGELOG.md index 372842375..1b67b7702 100644 --- a/packages/plugin-session-replay-browser/CHANGELOG.md +++ b/packages/plugin-session-replay-browser/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.8.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.7.1...@amplitude/plugin-session-replay-browser@0.8.1) (2023-11-16) + +**Note:** Version bump only for package @amplitude/plugin-session-replay-browser + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [0.7.1](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/plugin-session-replay-browser@0.7.1-beta.0...@amplitude/plugin-session-replay-browser@0.7.1) (2023-11-16) **Note:** Version bump only for package @amplitude/plugin-session-replay-browser diff --git a/packages/plugin-session-replay-browser/package.json b/packages/plugin-session-replay-browser/package.json index ac5de50c5..ca73fe88e 100644 --- a/packages/plugin-session-replay-browser/package.json +++ b/packages/plugin-session-replay-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/plugin-session-replay-browser", - "version": "0.8.0", + "version": "0.8.1", "description": "", "author": "Amplitude Inc", "homepage": "https://github.com/amplitude/Amplitude-TypeScript", From 09b33b71ab885e8c954fe28c1e1e54360784755b Mon Sep 17 00:00:00 2001 From: Jackson Huang Date: Tue, 5 Dec 2023 09:11:01 -0800 Subject: [PATCH 212/214] feat: expose rrweb blockselector --- packages/plugin-session-replay-browser/src/session-replay.ts | 3 +++ .../src/typings/session-replay.ts | 4 ++++ packages/session-replay-browser/src/config.ts | 4 ++++ packages/session-replay-browser/src/session-replay.ts | 2 ++ .../session-replay-browser/src/typings/session-replay.ts | 5 +++++ 5 files changed, 18 insertions(+) diff --git a/packages/plugin-session-replay-browser/src/session-replay.ts b/packages/plugin-session-replay-browser/src/session-replay.ts index ab76b1c30..d57b882a1 100644 --- a/packages/plugin-session-replay-browser/src/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/session-replay.ts @@ -46,6 +46,9 @@ export class SessionReplayPlugin implements EnrichmentPlugin { flushMaxRetries: this.config.flushMaxRetries, serverZone: this.config.serverZone, sampleRate: this.options.sampleRate, + privacyConfig: { + blockSelector: this.options.privacyConfig?.blockSelector, + }, }).promise; } diff --git a/packages/plugin-session-replay-browser/src/typings/session-replay.ts b/packages/plugin-session-replay-browser/src/typings/session-replay.ts index e4cfd5f5a..e31f92b82 100644 --- a/packages/plugin-session-replay-browser/src/typings/session-replay.ts +++ b/packages/plugin-session-replay-browser/src/typings/session-replay.ts @@ -1,3 +1,7 @@ +export interface SessionReplayPrivacyConfig { + blockSelector?: string | string[]; +} export interface SessionReplayOptions { sampleRate?: number; + privacyConfig?: SessionReplayPrivacyConfig; } diff --git a/packages/session-replay-browser/src/config.ts b/packages/session-replay-browser/src/config.ts index f1863c558..ac77d37b6 100644 --- a/packages/session-replay-browser/src/config.ts +++ b/packages/session-replay-browser/src/config.ts @@ -41,5 +41,9 @@ export class SessionReplayConfig extends Config implements ISessionReplayConfig } else { this.loggerProvider.error('Please provide both sessionId and deviceId.'); } + + if (options.privacyConfig && options.privacyConfig.blockSelector) { + //TODO: validate the selector. + } } } diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index 36ab4281c..7a6984235 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -271,6 +271,8 @@ export class SessionReplay implements AmplitudeSessionReplay { maskAllInputs: true, maskTextClass: MASK_TEXT_CLASS, blockClass: BLOCK_CLASS, + // rrweb only exposes array type through its types, but arrays are also be supported + blockSelector: this.config?.privacyConfig?.blockSelector as string, maskInputFn, recordCanvas: false, errorHandler: (error) => { diff --git a/packages/session-replay-browser/src/typings/session-replay.ts b/packages/session-replay-browser/src/typings/session-replay.ts index f36736c9c..b1064dc9c 100644 --- a/packages/session-replay-browser/src/typings/session-replay.ts +++ b/packages/session-replay-browser/src/typings/session-replay.ts @@ -31,6 +31,10 @@ export interface IDBStore { [sessionId: number]: IDBStoreSession; } +export interface SessionReplayPrivacyConfig { + blockSelector?: string | string[]; +} + export interface SessionReplayConfig extends Config { apiKey: string; deviceId?: string; @@ -40,6 +44,7 @@ export interface SessionReplayConfig extends Config { flushMaxRetries: number; sampleRate: number; sessionReplayId?: string; + privacyConfig?: SessionReplayPrivacyConfig; } export type SessionReplayOptions = Omit, 'apiKey'>; From 98d1bd43db9ba12c5cf5560d2c3928c257234f35 Mon Sep 17 00:00:00 2001 From: Jackson Huang Date: Tue, 5 Dec 2023 09:18:09 -0800 Subject: [PATCH 213/214] chore: remove validation logic due to perf concern --- packages/session-replay-browser/src/config.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/session-replay-browser/src/config.ts b/packages/session-replay-browser/src/config.ts index ac77d37b6..f1863c558 100644 --- a/packages/session-replay-browser/src/config.ts +++ b/packages/session-replay-browser/src/config.ts @@ -41,9 +41,5 @@ export class SessionReplayConfig extends Config implements ISessionReplayConfig } else { this.loggerProvider.error('Please provide both sessionId and deviceId.'); } - - if (options.privacyConfig && options.privacyConfig.blockSelector) { - //TODO: validate the selector. - } } } From d71769af7f9ce80b47f3d646599a7b57404a9565 Mon Sep 17 00:00:00 2001 From: Jackson Huang Date: Tue, 5 Dec 2023 10:24:32 -0800 Subject: [PATCH 214/214] chore: fix tests and coverage --- .../test/session-replay.test.ts | 6 ++++ packages/session-replay-browser/src/config.ts | 11 +++++- .../src/session-replay.ts | 8 +++-- .../test/session-replay.test.ts | 36 +++++++++++++++++-- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/packages/plugin-session-replay-browser/test/session-replay.test.ts b/packages/plugin-session-replay-browser/test/session-replay.test.ts index 31e2bb76c..5cec13224 100644 --- a/packages/plugin-session-replay-browser/test/session-replay.test.ts +++ b/packages/plugin-session-replay-browser/test/session-replay.test.ts @@ -104,6 +104,9 @@ describe('SessionReplayPlugin', () => { test('should call initalize on session replay sdk', async () => { const sessionReplay = new SessionReplayPlugin({ sampleRate: 0.4, + privacyConfig: { + blockSelector: ['#id'], + }, }); await sessionReplay.setup(mockConfig); @@ -119,6 +122,9 @@ describe('SessionReplayPlugin', () => { sampleRate: 0.4, serverZone: mockConfig.serverZone, sessionId: mockConfig.sessionId, + privacyConfig: { + blockSelector: ['#id'], + }, }); }); }); diff --git a/packages/session-replay-browser/src/config.ts b/packages/session-replay-browser/src/config.ts index f1863c558..715ee4b42 100644 --- a/packages/session-replay-browser/src/config.ts +++ b/packages/session-replay-browser/src/config.ts @@ -1,7 +1,11 @@ import { FetchTransport } from '@amplitude/analytics-client-common'; import { Config, Logger } from '@amplitude/analytics-core'; import { LogLevel } from '@amplitude/analytics-types'; -import { SessionReplayConfig as ISessionReplayConfig, SessionReplayOptions } from './typings/session-replay'; +import { + SessionReplayConfig as ISessionReplayConfig, + SessionReplayOptions, + SessionReplayPrivacyConfig, +} from './typings/session-replay'; import { DEFAULT_SAMPLE_RATE } from './constants'; import { generateSessionReplayId } from './helpers'; @@ -18,6 +22,7 @@ export class SessionReplayConfig extends Config implements ISessionReplayConfig deviceId?: string | undefined; sessionId?: number | undefined; sessionReplayId?: string | undefined; + privacyConfig?: SessionReplayPrivacyConfig; constructor(apiKey: string, options: SessionReplayOptions) { const defaultConfig = getDefaultConfig(); @@ -41,5 +46,9 @@ export class SessionReplayConfig extends Config implements ISessionReplayConfig } else { this.loggerProvider.error('Please provide both sessionId and deviceId.'); } + + if (options.privacyConfig) { + this.privacyConfig = options.privacyConfig; + } } } diff --git a/packages/session-replay-browser/src/session-replay.ts b/packages/session-replay-browser/src/session-replay.ts index 7a6984235..57d09a9f4 100644 --- a/packages/session-replay-browser/src/session-replay.ts +++ b/packages/session-replay-browser/src/session-replay.ts @@ -218,6 +218,10 @@ export class SessionReplay implements AmplitudeSessionReplay { return true; } + getBlockSelectors(): string | string[] | undefined { + return this.config?.privacyConfig?.blockSelector; + } + sendStoredEvents(storedReplaySessions: IDBStore) { for (const sessionId in storedReplaySessions) { const storedSequences = storedReplaySessions[sessionId].sessionSequences; @@ -271,8 +275,8 @@ export class SessionReplay implements AmplitudeSessionReplay { maskAllInputs: true, maskTextClass: MASK_TEXT_CLASS, blockClass: BLOCK_CLASS, - // rrweb only exposes array type through its types, but arrays are also be supported - blockSelector: this.config?.privacyConfig?.blockSelector as string, + // rrweb only exposes array type through its types, but arrays are also be supported. #class, ['#class', 'id'] + blockSelector: this.getBlockSelectors() as string, maskInputFn, recordCanvas: false, errorHandler: (error) => { diff --git a/packages/session-replay-browser/test/session-replay.test.ts b/packages/session-replay-browser/test/session-replay.test.ts index d618bb07b..954200e5d 100644 --- a/packages/session-replay-browser/test/session-replay.test.ts +++ b/packages/session-replay-browser/test/session-replay.test.ts @@ -98,6 +98,25 @@ describe('SessionReplayPlugin', () => { expect(sessionReplay.storageKey).toBe('AMP_replay_unsent_static_key'); }); + test('should setup plugin with privacy config', async () => { + const sessionReplay = new SessionReplay(); + await sessionReplay.init(apiKey, { + ...mockOptions, + sampleRate: 0.5, + privacyConfig: { blockSelector: ['.class', '#id'] }, + }).promise; + expect(sessionReplay.config?.transportProvider).toBeDefined(); + expect(sessionReplay.config?.flushMaxRetries).toBe(1); + expect(sessionReplay.config?.optOut).toBe(false); + expect(sessionReplay.config?.sampleRate).toBe(0.5); + expect(sessionReplay.config?.deviceId).toBe('1a2b3c'); + expect(sessionReplay.config?.sessionId).toBe(123); + expect(sessionReplay.config?.logLevel).toBe(0); + expect(sessionReplay.config?.privacyConfig?.blockSelector).toEqual(['.class', '#id']); + expect(sessionReplay.loggerProvider).toBeDefined(); + expect(sessionReplay.storageKey).toBe('AMP_replay_unsent_static_key'); + }); + test('should call initalize with shouldSendStoredEvents=true', async () => { const sessionReplay = new SessionReplay(); const initalize = jest.spyOn(sessionReplay, 'initialize').mockReturnValueOnce(Promise.resolve()); @@ -434,7 +453,11 @@ describe('SessionReplayPlugin', () => { }); test('should record events', async () => { const sessionReplay = new SessionReplay(); - sessionReplay.config = { apiKey, ...mockOptions } as SessionReplayConfig; + sessionReplay.config = { + apiKey, + ...mockOptions, + privacyConfig: { blockSelector: ['#id'] }, + } as SessionReplayConfig; const mockGetResolution = Promise.resolve({}); get.mockReturnValueOnce(mockGetResolution); await sessionReplay.initialize(); @@ -686,7 +709,8 @@ describe('SessionReplayPlugin', () => { test('should return early if user opts out', async () => { const sessionReplay = new SessionReplay(); - await sessionReplay.init(apiKey, { ...mockOptions, optOut: true }).promise; + await sessionReplay.init(apiKey, { ...mockOptions, optOut: true, privacyConfig: { blockSelector: ['#class'] } }) + .promise; sessionReplay.recordEvents(); expect(record).not.toHaveBeenCalled(); expect(sessionReplay.events).toEqual([]); @@ -1799,4 +1823,12 @@ describe('SessionReplayPlugin', () => { expect(url).toEqual(''); }); }); + + describe('getBlockSelectors', () => { + test('null config', () => { + const sessionReplay = new SessionReplay(); + sessionReplay.config = undefined; + expect(sessionReplay.getBlockSelectors()).not.toBeDefined(); + }); + }); });