├── .babelrc ├── .esdoc.json ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build-and-test.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.md ├── README-JP.md ├── README.md ├── dist └── c2p.js ├── docs ├── CONFIGURATION-JP.md ├── CONFIGURATION.md ├── CONTRIBUTORS.md ├── FAQ-JP.md ├── FAQ.md ├── METHODS-JP.md └── METHODS.md ├── eslint.config.mjs ├── package.json ├── pnpm-lock.yaml ├── rollup.config.js ├── src ├── index.dist.js ├── index.js └── utils.js ├── test ├── index.test.js ├── test.html └── utils.test.js ├── webpack.common.js ├── webpack.dev.js └── webpack.prd.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-inline-environment-variables", 4 | ["@babel/plugin-proposal-decorators", {"legacy": true}] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./esdocs", 4 | "plugins": [ 5 | { 6 | "name": "esdoc-standard-plugin" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "globals": { 7 | "process": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "parserOptions": { 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | "indent": [ 15 | "warn", 16 | 4 17 | ], 18 | "linebreak-style": [ 19 | "warn", 20 | "unix" 21 | ], 22 | "quotes": [ 23 | "warn", 24 | "single" 25 | ], 26 | "semi": [ 27 | "warn", 28 | "always" 29 | ], 30 | "no-console": "off" 31 | } 32 | }; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Payload comparison** 21 | Actual payload JSON 22 | ``` 23 | {} 24 | ``` 25 | 26 | Expected payload JSON 27 | ``` 28 | {} 29 | ``` 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build-and-test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 22 14 | - uses: pnpm/action-setup@v4 15 | with: 16 | version: 10 17 | - run: pnpm install 18 | - run: pnpm test 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release artifact 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | build_dist: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node: [22] 13 | steps: 14 | - name: checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - uses: pnpm/action-setup@v4 24 | with: 25 | version: 10 26 | - name: pnpm install, build:dist 27 | env: 28 | SDK_API_KEY: ${{ secrets.SDK_API_KEY }} 29 | DEFAULT_ENDPOINT: ${{ secrets.DEFAULT_ENDPOINT }} 30 | run: | 31 | pnpm install 32 | pnpm build:dist 33 | 34 | - name: zip dist 35 | run: | 36 | cd dist 37 | zip release atj.* 38 | 39 | - name: Create release 40 | id: create_release 41 | uses: actions/create-release@v1.0.0 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | with: 45 | tag_name: ${{ github.ref }} 46 | release_name: Release ${{ github.ref }} 47 | draft: false 48 | prerelease: false 49 | 50 | - name: Upload Release Asset 51 | id: upload-release-asset 52 | uses: actions/upload-release-asset@v1.0.1 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | with: 56 | upload_url: ${{ steps.create_release.outputs.upload_url }} 57 | asset_path: ./dist/release.zip 58 | asset_name: atj.zip 59 | asset_content_type: application/zip -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swp 3 | .DS_Store 4 | test/integration/pages/assets/*.js 5 | dist/atj.js 6 | dist/atj.min.js 7 | dist/index.js 8 | dist/atj.zip 9 | dist/index.html 10 | test/build -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ATJ Change Log 2 | 3 | ## 2021 4 | 5 | ### 2021.9.22 6 | 7 | - `trackClick` オプションが設定されていない場合にエラーが発生する問題を修正 8 | 9 | ### 2021.9.2 10 | 11 | #### v2.15.6 12 | 13 | - Cookie読み出し用ユーティリティ関数 `getC()` が戻す値が途切れる問題を修正 14 | 15 | ### 2021.9.1 16 | 17 | #### v.2.15.4 18 | 19 | - `this.contentWindow === this` が `false` の場合に `new AbortController()` 呼び出しが機能しない問題を修正 20 | 21 | ### 2021.7.16 22 | #### v 2.15.2 23 | 24 | - v2.14以降にクリック計測で `ingest.context.action.name` が `undefined` になる問題を修正  25 | 26 | ### 2021.6.2 27 | 28 | - クリック計測時にコンソールにエラーが発生してしまう問題を修正 29 | 30 | ### 2021.4.23 31 | 32 | #### v2.15.0 33 | 34 | - データ送信に用いるメソッドをGETに統一。国内ではPOSTでの送信ができないケースがあり、実質GETでのみ利用されている 35 | - Atlas IDのドメイン間引き継ぎ機能を廃止 36 | - SDK組み込みのデバッグ支援機能を廃止 37 | - クリック計測、ダウンロード計測、離脱リンク計測において送信する属性を拡張、同時に計測機構を簡素化 38 | - FetchならびにXHR呼び出しをtry-catch化 39 | - `postMessage` 経由でiframe内からの計測イベントを受け取れる機構を導入 40 | 41 | ### 2021.3.24 42 | 43 | #### v2.14.2 44 | 45 | - `system.targetWindow` オプション指定により、iframe内で動かす場合等にどのwindowを計測対象とするかを選択可能とした 46 | - SDK内のオブジェクト構造を最適化 47 | 48 | ### 2021.2.26 49 | 50 | #### v2.14.1 51 | 52 | - クリック計測対象を絞り込み 53 | 54 | ### 2021.2.25 55 | 56 | #### v2.14.0 57 | 58 | - `trackClick` が有効かつ `targetAttribute` が未指定の場合に、リンクとボタンに対するクリックを自動計測するように改良 59 | 60 | 61 | ## 2019 62 | 63 | ### 2019-06-13 64 | 65 | #### v2.13.0 66 | 67 | - JavascriptからCookieへの書き込みをしていた箇所を削除 68 | - デバイスIDをLocalstorageへバックアップする処理を追加 69 | 70 | ### 2019-02-20 71 | 72 | #### v2.12.7 73 | 74 | - デバッグ用のCookieの値にpathが設定されていない問題を修正 75 | - デバッグ時のコンソールへのログに含まれる値が不十分だった問題を修正 76 | 77 | ### 2019-01-16 78 | 79 | #### v2.12.6 80 | 81 | - IEとの互換性について改良 82 | - デバッグ用コンソール出力を `console.dir()` に変更、加えて出力内容をペイロードそのものに変更 83 | - デバッグ用 `debug()` メソッドを廃止、デバッグする場合は手動で `atlasOutputLog` Cookieに `true` を指定する 84 | - `ingest.context.root_id` にページビューイベントごとにユニークIDを付与する機構を追加 85 | - `ingest.context.id` の利用を廃止。当初はリクエストごとにユニークIDを入れることを想定していたが、エンドポイントがペイロードからハッシュ値を生成するため必要性がない状態となっていた 86 | 87 | ## 2018 88 | 89 | ### 2018-12-12 90 | 91 | #### v2.12.5 92 | 93 | - 脆弱性が報告されていたプラグインを更新 94 | 95 | ### 2018-10-30 96 | 97 | #### v2.12.4 98 | 99 | - `site_search` を `search` に改名(アプリ他ウェブサイト以外にも汎用的に適用できる命名) 100 | 101 | ### 2018-10-30 102 | 103 | #### v2.12.3 104 | - 複数の計測機能において `dataset` 変数の階層を1段繰り上げた 105 | - `trackClick` が指定のデータ属性に基づいて要素の階層を表現し `context.action.name` に格納するようにした 106 | - `trackAction` の引数として渡すカスタムオブジェクトにおいて、`custom_value` を `custom_vars` に置き換えた(送信時の変数名との整合性) 107 | 108 | ### 2018-10-05 109 | 110 | #### v2.12.2 111 | - localStorageから値を取得する `getLocalStorageValue` メソッドを追加 112 | 113 | ### 2018-09-10 114 | 115 | #### v2.12.1 116 | 117 | ##### Improvements 118 | - クリック計測の Event Delegationのターゲットをdocumentではなくbodyに変更 119 | - クリック計測において touchstart のバインディングをやめた(Clickとして成立していないのに計測してしまう、スクロール時のパフォーマンスに影響する) 120 | 121 | ### 2018-09-07 122 | 123 | #### v2.12.0 124 | 125 | ##### New Features 126 | - フォーム分析に `trackForm` を追加 127 | 128 | ### 2018-09-03 129 | 130 | #### v2.11.0 131 | 132 | ##### Breaking Changes 133 | - `setDataSrc` メソッドを廃止 134 | - `observeMutation` オプションを廃止 135 | - メディア計測の対象をNodeListではなく、クエリーセレクター文字列を渡す形に変更 136 | 137 | ##### New Features 138 | - `data-trackable` によるクリック計測 139 | 140 | ##### Improvements 141 | - リンク処理をEvent Delegation化 142 | - メディア計測をEvent Delegation化 143 | - Google Analyticsで一般的な `utm_` 系キャンペーン計測変数をサポート 144 | - 内部で利用する変数名を短縮しファイルサイズを縮小 145 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | RUN mkdir -p /var/atj 4 | 5 | ADD ./ /var/atj 6 | WORKDIR /var/atj 7 | 8 | RUN apk add python3 make g++ openjdk8-jre chromium grep 9 | 10 | ENV PATH $PATH:/var/atj/node_modules 11 | 12 | RUN npm install 13 | 14 | CMD ["/bin/sh"] 15 | 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | -- 3 | 4 | Copyright (c) 2018 Nikkei Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README-JP.md: -------------------------------------------------------------------------------- 1 | # Atlas Tracking JS (ATJ) 2 | 3 | 一般的なウェブサイト用 Atlas計測ライブラリ 4 | 5 | - ATJはAtlas用の計測SDKです 6 | - ATJは、一般的な用途のウェブ解析用ビーコンを送信します 7 | - ATJはブラウザーの現代的な機能である、sendBeacon、requestAnimationFrame、そしてNavigation Timing等を活用します 8 | - いくつかの機能は Event Delegation の仕組みを利用します 9 | - 豊富な自動計測のオプションが用意されています 10 | 11 | ## ドキュメント 12 | 13 | - [英語の説明](./README.md) はこちら ([README in English](./README.md) is available.) 14 | - [設定ガイド](./docs/CONFIGURATION-JP.md) では、c2p.jsを通じて設定可能な全ての機能について説明しています 15 | - ユーティリティ関数がATJコアに用意されています。 [関数一覧](./docs/METHODS-JP.md)から便利なメソッドを見付けられます 16 | - [FAQ](./docs/FAQ-JP.md) はこちらですが、発展途上です 17 | - ATJ は [MITライセンス](./LICENSE.md)のもとで公開しています 18 | - このプロジェクトの [全てのコントリビューター](./docs/CONTRIBUTORS.md) に感謝します 19 | - 現時点では、運用ポリシーにより、日経社外からのプルリクエストはマージされません。もし問題を発見した場合は、GitHubでイシューを作成してください 20 | 21 | ## ATJのビルド 22 | 23 | #### ビルド環境をDockerで構築 24 | ```sh 25 | docker build \ 26 | --platform linux/x86_64 \ 27 | --tag atj \ 28 | ./ 29 | 30 | docker run \ 31 | --platform linux/x86_64 \ 32 | --name ATJ \ 33 | --env SDK_API_KEY=your_sdk_api_key \ 34 | --env DEFAULT_ENDPOINT=your.atlas.endpoint \ 35 | --env SDK_NAMESPACE=atlasTracking \ 36 | --volume ${PWD##}:/var/atj \ 37 | -it \ 38 | atj 39 | ``` 40 | 41 | ### 環境変数 42 | |環境変数|目的|デフォルト|例| 43 | |:---:|:---:|:---:|:---:| 44 | |SDK_API_KEY|データ送信先であるエンドポイントでの認証に用いる|`test_api_key`|`abc123xyz789`| 45 | |DEFAULT_ENDPOINT|デフォルトのエンドポイント。c2p.jsの設定変数でエンドポイントを指定しない場合、この値でフォールバックされる|`atlas.local`|`atlas-endpoint.your.domain`| 46 | |SDK_NAMESPACE|ATJで使う全ての関数や変数が格納される名前空間、よってATJはグローバル名前空間を1つだけ使う|`atlasTracking`|`atlasTracking`| 47 | 48 | #### 初期設定 49 | ```sh 50 | pnpm install 51 | ``` 52 | 53 | ### テスト 54 | - 構文チェックは `pnpm run eslint` でできます 55 | - ユニットテストを行う場合は `pnpm run test` を実行 56 | 57 | ### ビルド 58 | - スタンドアロンのATJは `pnpm run build:dist` (一般的に、これはほとんどの用途に適合します) 59 | - NPMモジュールを生成する場合は, `pnpm run build:npm` 60 | 61 | ## 実装ガイド 62 | 63 | ### 必須要件 64 | - グローバル名前空間から一つの変数が、ATJ関連の変数やメソッドを格納するために必要。この名前はATJをビルドする際に指定できる 65 | - ATJは一意なブラウザを識別するためにCookieを一つ利用する。Cookieの名称は c2p.js で指定できるが、デフォルトでは `atlasId` 66 | - ATJは、スクロール深度、読了、ビューワブルインプレッションの検出のため、`atlasVisibilityStatus` という名前のカスタムイベントを発生させる。発生頻度は `requestAnimationFrame` とスロットリングの組み合わせに依存する 67 | - ATJはpolyfill無しに可能な限り多くのブラウザをサポートしている。ただし、IE9以前の古いブラウザでは動作しない 68 | 69 | ### 基本的な導入方法 70 | 1. [Readme](./README-JP.md) に従ってATJをビルドすると、`./dist` ディレクトリの中に `atj.min.js` が生成される 71 | 2. `c2p.js` という名前の設定ファイルを `./dist` ディレクトリ内のサンプルファイルを基に作成する。[設定ガイド](./docs/CONFIGURATION-JP.md) を読んでカスタマイズする 72 | 3. `atj.min.js` と `c2p.js` の両ファイルをウェブサイトの各ページの `...` 内で読み込む 73 | - これらのファイルを、`` のように `script` タグを使って直接埋め込む方法 74 | - Adobe Dynamic Tag Management や Google Tag Manager といったタグマネジメントツールを通じて展開する方法 75 | 4. ウェブサイトをチェックして、スクリプトエラーが起こらず、ATJがビーコンを送信していることを確認する 76 | 77 | ### c2p.jsの内側 78 | - c2p.js は以下の順でメソッドを発火する: 79 | 1. `config()` を呼び出してATJを初期設定 80 | 2. ページ個別のデータを初期化するために `initPage()` を呼び出す 81 | 3. そして、ページビューを計測するために `trackPage()` を呼び出す 82 | - また、c2p.js はデータ取得やデータの準備のためのカスタムコードを含むことができる 83 | - もしウェブサイトがSPA(Single Page Application)の場合、 `initPage()` と `trackPage()` を画面が変更される度に呼び出すことができる (ATJそのものの初期化をやり直す必要は無い) 84 | 85 | ### プライバシーのためのオプトアウト機能 86 | ATJは「計測からオプトアウト」をサービス上で実現する機能が組み込まれている。これはいかなる場合でもプライバシーの観点から推奨され、もしGDPRが適用される国へサービスを提供する場合にはオプトアウト機能を提供することを検討する必要がある。 87 | 88 | 1. 「計測からオプトアウト」ボタンを追加するために、ページやモーダルを追加または編集する 89 | 2. `atlasTracking.optout('enable')` を呼ぶための数行のコードを書き、ユーザーがオプトアウトボタンをクリックしたら発火するようにする 90 | 91 | - オプトアウトしたユーザーは `atlasTracking.optout('disable');` によってオプトインできる 92 | - ATJの範囲内でユーザーのプライバシーを確実に保護するため、オプトアウト機能は最初のビーコンが送られるよりも前に設置される必要がある。従って、 `initPage()` と `trackPage()` はユーザーが計測に合意した後に発動すべきである 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atlas Tracking JS (ATJ) 2 | 3 | Atlas tracking library for general web site 4 | 5 | - ATJ is a tracking SDK for Atlas 6 | - ATJ transmits beacons for general purpose of web analytics 7 | - ATJ utilizes web-browser's modern features such as sendBeacon, requestAnimationFrame and Navigation Timing 8 | - Some features uses Event Delegation mechanism 9 | - A rich variety of auto tracking options available 10 | 11 | ## Documents 12 | 13 | - [README in Japanese](./README-JP.md) is available. ([日本語の説明](./README-JP.md) はこちら) 14 | - [Configuration Guide](./docs/CONFIGURATION.md) describes all customizable features managed through c2p.js. 15 | - Some utility functions are available within ATJ core. You will find useful methods from a [List of Methods](./docs/METHODS.md) 16 | - [FAQ](./docs/FAQ.md) is here but is still under enhancement. 17 | - ATJ is published under [MIT License](./LICENSE.md) 18 | - Special thanks to [all contributors](./docs/CONTRIBUTORS.md) in this project. 19 | - As of today, we won't merge pull-requests from outside of Nikkei due to operational policy. Please create an issue on GitHub if you find any problems. 20 | 21 | ## Build ATJ 22 | 23 | #### Create a build environment on Docker 24 | ```sh 25 | docker build \ 26 | --platform linux/x86_64 \ 27 | --tag atj \ 28 | ./ 29 | 30 | docker run \ 31 | --platform linux/x86_64 \ 32 | --name ATJ \ 33 | --env SDK_API_KEY=your_sdk_api_key \ 34 | --env DEFAULT_ENDPOINT=your.atlas.endpoint \ 35 | --env SDK_NAMESPACE=atlasTracking \ 36 | --volume ${PWD##}:/var/atj \ 37 | -it \ 38 | atj 39 | ``` 40 | 41 | #### Environment Variables 42 | |Variable|Purpose|Default|Example| 43 | |:---:|:---:|:---:|:---:| 44 | |SDK_API_KEY|For the authentication at Atlas Endpoint which is a destination to put data to|`test_api_key`|`abc123xyz789`| 45 | |DEFAULT_ENDPOINT|A default endpoint. If you don't specify the endpoint in config variables in c2p.js, ATJ fall backs to the endpoint to this value|`atlas.local`|`atlas-endpoint.your.domain`| 46 | |SDK_NAMESPACE|A namespace which will contains all methods and variables for ATJ, so ATJ consumes just one global namespace.|`atlasTracking`|`atlasTracking`| 47 | 48 | #### Initialization 49 | ```sh 50 | pnpm install 51 | ``` 52 | 53 | ### Test 54 | - You can lint the code by `pnpm run eslint` 55 | - For the unit test, run `pnpm run test` 56 | 57 | ### Build 58 | - For the standalone ATJ, `pnpm run build:dist` (In general, this will fit with most use cases) 59 | - For generating NPM module, `pnpm run build:npm` 60 | 61 | ## Implementation Guide 62 | 63 | ### Requirements 64 | - One variable in global namespace is required to store ATJ related variables and methods. You can specify the name for this when you build ATJ. 65 | - ATJ uses one Cookie to identify unique browser. A Cookie name is defined in c2p.js but the default is `atlasId` 66 | - ATJ dispatch a custom event named `atlasVisibilityStatus` to detect visibility status of each elements for Scroll Depth, Read Through and Viewable Impression. Dispatch frequency depends on the combination of `requestAnimationFrame` and throttling. 67 | - ATJ supports many browsers as possible without polyfill, but it doesn't work with old Internet Explorer older than IE9 68 | 69 | ### Basic Installation 70 | 1. Build your ATJ by following [Readme](./README.md) and then you will have `atj.min.js` in `./dist` directory. 71 | 2. Create a config file named `c2p.js` based on a sample file in `./dist` directory. Read [Config Guide](./docs/CONFIGURATION.md) carefully to customize ATJ. 72 | 3. Load both files `atj.min.js` and `c2p.js` within `...` on each page in your web site. 73 | - You can call these files by embedding `script` tags like `` directly. 74 | - Also, you can deploy ATJ files through Tag Management tools such as Adobe Dynamic Tag Management, Google Tag Manager and other tag management tools. 75 | 4. Check your web site to confirm that there is no script errors and ATJ is sending beacons. 76 | 77 | ### Inside c2p.js 78 | - c2p.js fires methods as below step: 79 | 1. Configure ATJ core by calling `config()` 80 | 2. Initialize the page data by calling `initPage()` 81 | 3. Then, send a beacon for tracking pageview by `trackPage()` 82 | - Also, c2p.js is able to have some custom codes to retrieve and/or prepare data. 83 | - If your web site is SPA (Single Page Application), you can call `initPage()` and `trackPage()` each times when screen has been changed. (no need to re-initialize ATJ itself) 84 | 85 | ### Opt-out for Privacy 86 | ATJ has a built-in method to add "opt-out from tracking" feature in your service. This is strongly recommended at any cases from the privacy perspective. It's necessary to consider the opt-out feature if your service is used from countries which is applied GDPR. 87 | 88 | 1. Create or modify your opt-out page or modal to add an "opt-out from tracking" button. 89 | 2. Write a few code to call `atlasTracking.optout('enable')` when user clicks the opt-out button. 90 | 91 | - Opt-outed user can opt-in by `atlasTracking.optout('disable');` 92 | - To protect user's privacy perfectly within ATJ, you should place the opt-out feature before sending the first beacon, thus `initPage()` and `trackPage()` must be triggered once user has accepted tracking. 93 | -------------------------------------------------------------------------------- /dist/c2p.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // Cutting The Mustard! 3 | if ('querySelector' in window.parent.document && 4 | 'addEventListener' in window.parent 5 | ) { 6 | 7 | // Configure 8 | atlasTracking.config({ 9 | 'system': { 10 | 'endpoint': 'CHANGE_ME', 11 | 'apiKey': 'CHANGE_ME', 12 | 'beaconTimeout': 2000, 13 | 'cookieMaxAge': (2 * 365 * 24 * 60 * 60), 14 | 'cookieDomain': 'CHANGE_ME', 15 | 'targetWindow': 'self' 16 | }, 17 | 'defaults': { 18 | 'pageUrl': window.parent.document.location.href, 19 | 'pageReferrer': window.parent.document.referrer, 20 | 'pageTitle': window.parent.document.title, 21 | }, 22 | 'product': { 23 | 'productFamily': 'CHANGE_ME', 24 | 'productName': 'CHANGE_ME' 25 | }, 26 | 'options': { 27 | 'trackClick': { 28 | 'enable': true, 29 | 'targetAttribute': 'data-atlas-trackable', 30 | 'disableText': false, 31 | 'logAllClicks': true, 32 | 'logLastClick': true, 33 | 'lastClickTtl': 5, 34 | 'useLastClickOnly': false, 35 | }, 36 | 'trackLink': { 37 | 'enable': true, 38 | 'internalDomains': ['CHANGE_ME'], 39 | 'nameAttribute': 'data-atlas-link-name', 40 | }, 41 | 'trackDownload': { 42 | 'enable': true, 43 | 'fileExtensions': ['pdf', 'zip', 'laz', 'tar', 'gz', 'tgz', 'docx', 'xlsx', 'pptx', 'doc', 'xls', 'ppt'], 44 | 'nameAttribute': 'data-atlas-link-name', 45 | }, 46 | 'trackNavigation': { 47 | 'enable': true, 48 | }, 49 | 'trackPerformance': { 50 | 'enable': true, 51 | }, 52 | 'trackScroll': { 53 | 'enable': true, 54 | 'granularity': 20, 55 | 'threshold': 2, 56 | }, 57 | 'trackInfinityScroll': { 58 | 'enable': false, 59 | 'step': 600, 60 | 'threshold': 2, 61 | }, 62 | 'trackRead': { 63 | 'enable': false, 64 | 'target': null, 65 | 'granularity': 25, 66 | 'milestones': [4, 15, 30, 60, 90, 120], 67 | }, 68 | 'trackViewability': { 69 | 'enable': false, 70 | 'targets': [], 71 | }, 72 | 'trackMedia': { 73 | 'enable': false, 74 | 'selector': 'video, audio', 75 | 'heartbeat': 5, 76 | }, 77 | 'trackForm': { 78 | 'enable': false, 79 | 'target': null, 80 | }, 81 | 'trackUnload': { 82 | 'enable': true, 83 | }, 84 | 'trackThroughMessage': { 85 | 'enable': true, 86 | } 87 | } 88 | }); 89 | 90 | // Init Page data 91 | atlasTracking.initPage({ 92 | user: { 93 | 'user_id': undefined, 94 | 'user_status': undefined, 95 | 'site_session': undefined 96 | }, 97 | context: { 98 | 'app': undefined, 99 | 'app_version': undefined, 100 | 'source': undefined, 101 | 'edition': undefined, 102 | 'content_id': undefined, 103 | 'content_name': undefined, 104 | 'content_status': undefined, 105 | 'page_name': undefined, 106 | 'page_num': undefined, 107 | 'category_l1': undefined, 108 | 'category_l2': undefined, 109 | 'category_l3': undefined, 110 | 'tracking_code': atlasTracking.getQueryValue('cid'), 111 | 'campaign': { 112 | 'name': decodeURIComponent(atlasTracking.getQueryValue('utm_campaign')) || undefined, 113 | 'source': decodeURIComponent(atlasTracking.getQueryValue('utm_source')) || undefined, 114 | 'medium': decodeURIComponent(atlasTracking.getQueryValue('utm_medium')) || undefined, 115 | 'term': decodeURIComponent(atlasTracking.getQueryValue('utm_term')) || undefined, 116 | 'content': decodeURIComponent(atlasTracking.getQueryValue('utm_content')) || undefined, 117 | }, 118 | 'search': { 119 | 'term': undefined, 120 | 'options': undefined, 121 | 'results': undefined 122 | }, 123 | 'events': undefined, 124 | 'custom_object': {}, 125 | 'funnel': {} 126 | } 127 | }); 128 | 129 | // Send PV 130 | atlasTracking.trackPage(); 131 | } 132 | }()); 133 | -------------------------------------------------------------------------------- /docs/CONFIGURATION-JP.md: -------------------------------------------------------------------------------- 1 | # 設定ガイド 2 | 3 | ## config() に対するライブラリレベルの設定引数 4 | 5 | `config()` は設定変数を内包する一つのオブジェクトを受け取る。 6 | `system` を除くほとんどの変数は省略可能だが、明示的に値を指定することを強く推奨する。 7 | 8 | ### 基本構造 9 | 10 | ```javascript 11 | { 12 | 'system': {...}, 13 | 'defaults': {...}, 14 | 'product': {...}, 15 | 'options': { 16 | 'exchangeAtlasId': {...}, 17 | 'trackClick': {...}, 18 | 'trackLink': {...}, 19 | 'trackDownload': {...}, 20 | 'trackNavigation': {...}, 21 | 'trackPerformance': {...}, 22 | 'trackScroll': {...}, 23 | 'trackInfinityScroll': {...}, 24 | 'trackRead': {...}, 25 | 'trackViewability': {...}, 26 | 'trackMedia': {...}, 27 | 'trackForm': {...}, 28 | 'trackUnload': {...} 29 | } 30 | } 31 | ``` 32 | 33 | ### 変数 34 | 35 | #### system 36 | 37 | |変数|型|目的|例| 38 | |:----:|:----:|:----:|:----:| 39 | |endpoint|String|ATJがビーコンを送る宛先|`atlas-endpoint.your.domain`| 40 | |apiKey|String|エンドポイントはこのキーを持つビーコンを受け付ける|`abc123xyz789`| 41 | |beaconTimeout|Integer|エンドポイントとの通信タイムアウトをミリ秒で指定|`2000` (2 sec)| 42 | |cookieName|String| **廃止** Atlas IDを保存するCookie名|`atlasId`| 43 | |cookieMaxAge|Integer| **廃止** Atlas IDのCookieの有効期間|`(2 * 365 * 24 * 60 * 60)` (2 years)| 44 | |cookieDomain|String| **廃止** Cookieを保存する際にドメイン属性として利用するドメイン名|`your.domain`| 45 | |targetWindow|String|ATJが動く(相対的な)ウィンドウ名|`parent`| 46 | 47 | #### defaults 48 | 49 | |変数|型|目的|例| 50 | |:----:|:----:|:----:|:----:| 51 | |pageUrl|String|ページビューその他のイベントが発生した場所を指すURL|`window.parent.document.location.href`| 52 | |pageReferrer|String|リファラーURL = 直前のページ|`window.parent.document.referrer`| 53 | |pageTitle|String|ページタイトル|`window.parent.document.title`| 54 | 55 | - ページタイトルはページを識別するために便利な反面、各ページで異なる値がセットされている必要がある。 56 | 57 | #### product 58 | 59 | |変数|型|目的|例| 60 | |:----:|:----:|:----:|:----:| 61 | |productFamily|String|製品ファミリー。サービスに対して複数のUIやアプリからアクセスできる場合、それらをグループ化する|`MyDigitalService`| 62 | |productName|String|製品名。同じブランドで複数のサービスを持っている場合、この値で個々のサービスを区別する|`MyDigitalService-Web`| 63 | 64 | - 1つのブランドのもとで1つの製品のみを持つ場合、両変数に同じ値をセットできる。 65 | 66 | #### useGet (オプション以下) 67 | 68 | - この機能は2.14.2まで利用可能、以降のバージョンでは存在しない。 69 | 70 | |変数|型|目的|例| 71 | |:----:|:----:|:----:|:----:| 72 | |useGet|Boolean|ビーコンを送信するためのメソッドの切り替え。 `true` = GET、 `false` = POST|`true`| 73 | 74 | - 日本固有のプロキシ型セキュリティソフトがPOSTボディを消すが、Content-Lengthを維持する。よって、セキュリティソフトにデータを破壊されないためにGETが安全な選択である。 75 | 76 | #### exchangeAtlasId (オプション以下) 77 | 78 | - この機能は2.14.2まで利用可能、以降のバージョンでは存在しない。 79 | 80 | |変数|型|目的|例| 81 | |:----:|:----:|:----:|:----:| 82 | |exchangeAtlasId.pass|Boolean|この機能を使うか否か|`true`| 83 | |exchangeAtlasId.passParamKey|String|Atlas IDをセットするGETパラメーターの名前|`atlas_id`| 84 | |exchangeAtlasId.passTargetDomains|Array|Atlas IDを渡すドメイン名の配列|`['domain1','domain2','domain3']`| 85 | |exchangeAtlasId.catch|Boolean|この機能を使うか否か|`true`| 86 | |exchangeAtlasId.catchParamKey|String|Atlas IDを受け取るGETパラメータの名前|`atlas_id`| 87 | |exchangeAtlasId.catchTargetDomains|Array|Atlas IDを受け取るドメイン名の配列|`['domain1','domain2','domain3']`| 88 | 89 | - ATJはAtlas IDを複数のドメインを跨いで、サードパーティCookie無しで共有することができ、Atlas IDを交換するためにGETパラメータを使う。 90 | - `pass` はAtlas IDを `passTargetDomains` に列挙された外部のドメインに引き渡す機能 91 | - `catch` はAtlas IDを `catchTargetDomains` に列挙された外部のドメインから受け取る機能 92 | 93 | #### trackClick (オプション以下) 94 | 95 | |変数|型|目的|例| 96 | |:----:|:----:|:----:|:----:| 97 | |trackClick.enable|Boolean|この機能を使うか否か|`true`| 98 | |trackClick.targetAttribute|String|ATJはユーザーがこのデータ属性を持つ要素をクリックしたときにデータを収集する|`data-atlas-trackable`| 99 | |trackClick.disableText|Boolean|クリックされた要素の文字列の記録を無効化するか否か|`false`| 100 | |trackClick.logLastClick|Boolean|クリックに関する情報を次のページに引き継ぎ遷移先で直前のクリックを計測する|`true`| 101 | |trackClick.lastClickTtl|Integer|直前のクリックについての情報の有効期間(秒)|`5`| 102 | |trackClick.useLastClickOnly|Boolean|直前のクリック計測のみを利用し、クリックイベント時の計測を無効化するか否か|`false`| 103 | |trackClick.logAllClicks|Boolean|全てのクリックを強制的に計測する。データ属性の指定と完全な自動計測を両立する|`true`| 104 | 105 | - `enable` が `true` であり、 `targetAttribute` に何らかの値が指定されている場合、 `targetAttribute` に指定されたdata属性を持つ要素のみが計測対象となる 106 | - `enable` が `true` であり、 `targetAttribute` が未指定または `false` の場合、全てのクリッカブル要素が計測対象となる 107 | - `enable` が `true` であり、 `targetAttribute` に何らかの値が指定され、かつ `logAllClicks` が存在し `true`がセットされている場合、全てのクリックが `targetAttribute` で指定されたdata属性により命名された上で計測される 108 | - `logLastClick` を用いる場合、 Session Storage にキー名 `atlasLastClick` でJSON文字列が格納される(次ページで削除される) 109 | 110 | #### trackLink (オプション以下) 111 | 112 | |変数|型|目的|例| 113 | |:----:|:----:|:----:|:----:| 114 | |trackLink.enable|Boolean|この機能を使うか否か|`true`| 115 | |trackLink.internalDomains|Array|離脱リンクのクリック計測から除外したいドメイン名の配列|`['domain1','domain2','domain3']`| 116 | |trackLink.nameAttribute|String|ここで指定したデータ属性を追加して、離脱リンクに任意の名前を設定できる|`data-atlas-link-name`| 117 | 118 | #### trackDownload (オプション以下) 119 | 120 | |変数|型|目的|例| 121 | |:----:|:----:|:----:|:----:| 122 | |trackDownload.enable|Boolean|この機能を使うか否か|`true`| 123 | |trackDownload.fileExtensions|Array|ダウンロード計測の対象とするファイル拡張子の配列|`['pdf','zip','tar','gz']`| 124 | |trackDownload.nameAttribute|String|ここで指定したデータ属性を追加して、ダウンロードリンクに任意の名前を設定できる|`data-atlas-link-name`| 125 | 126 | 127 | #### trackNavigation (オプション以下) 128 | 129 | |変数|型|目的|例| 130 | |:----:|:----:|:----:|:----:| 131 | |trackNavigation.enable|Boolean|この機能を使うか否か|`true`| 132 | 133 | 134 | #### trackPerformance (オプション以下) 135 | 136 | |変数|型|目的|例| 137 | |:----:|:----:|:----:|:----:| 138 | |trackPerformance.enable|Boolean|この機能を使うか否か|`true`| 139 | 140 | - Track Performanceは実際のユーザーのパフォーマンス情報を把握するのに役立つが、データオブジェクトは大きい。 141 | - onload までの時間を測定したい場合、読み込みの順序の関係でonloadイベントで `trackAction()` を呼び出す必要があるかもしれない。 142 | 143 | #### trackScroll (オプション以下) 144 | 145 | |変数|型|目的|例| 146 | |:----:|:----:|:----:|:----:| 147 | |trackScroll.enable|Boolean|この機能を使うか否か|`true`| 148 | |trackScroll.granularity|Integer|ATJはスクロール深度がここで指定したNパーセント(1-99)以上変化したときにビーコンを送信する|`20`| 149 | |trackScroll.threshold|Integer|ATJはユーザーがスクロール深度をここで定義したT秒維持したらビーコンを送信する|`2`| 150 | 151 | - スクロール深度は `granularity` と `threshold` の組み合わせで測定される。よって、ユーザーが90%までスクロールダウンしたが、1秒以内に10%にスクロールアップした場合、ATJはビーコンを送らない。 152 | - スクロール深度計測はし多方向へのスクロール動作のみを観測する。 153 | - もし無限スクロール/遅延読み込みを利用している場合、標準のスクロール深度計測は適さない。したがって `trackScroll` の代わりに `trackInfinityScroll` を使う。 154 | 155 | #### trackInfinityScroll (オプション以下) 156 | 157 | |変数|型|目的|例| 158 | |:----:|:----:|:----:|:----:| 159 | |trackInfinityScroll.enable|Boolean|この機能を使うか否か|`true`| 160 | |trackInfinityScroll.step|Integer|スクロール深度がここで指定したNピクセル/ポイント以上変化したときにATJはビーコンを送信する|`600`| 161 | |trackInfinityScroll.threshold|Integer|ATJはユーザーがスクロール深度をここで定義したT秒維持したらビーコンを送信する|`2`| 162 | 163 | #### trackRead (オプション以下) 164 | 165 | |変数|型|目的|例| 166 | |:----:|:----:|:----:|:----:| 167 | |trackRead.enable|Boolean|この機能を使うか否か|`true`| 168 | |trackRead.target|Element|観測対象となる要素|`document.getElementById('article_body')`| 169 | |trackRead.granularity|Integer|ATJは読了率がNパーセント(1-99)以上変化したときにビーコンを送信する|`25`| 170 | |trackRead.milestones|Array|ATJはまた、これらのマイルストーンを越えた時点でビーコンを送信する|`[4, 15, 30, 60, 90, 120]`| 171 | 172 | - `trackScroll` と `trackRead` の違いは: 173 | - `trackScroll` はwindowに対するスクロール深度を計測する 174 | - `trackRead` はコンテンツ本体のブロック要素における可視性の変化に注目する 175 | - `trackScroll` は深度と時間の組み合わせで動くが、 `trackRead` は深度と時間を切り離して扱う 176 | 177 | #### trackViewability (オプション以下) 178 | 179 | |変数|型|目的|例| 180 | |:----:|:----:|:----:|:----:| 181 | |trackViewability.enable|Boolean|この機能を使うか否か|`true`| 182 | |trackViewability.targets|Array|観測対象のエレメントの配列|`document.getElementsByClassName('ad_frame')`| 183 | 184 | - IABのビューワブルインプレッションの定義が適用されるが、ATJはパフォーマンスの観点から厳密な秒数を計らない。(250ミリ秒ごとに間引く) 185 | 186 | #### trackMedia (オプション以下) 187 | 188 | |変数|型|目的|例| 189 | |:----:|:----:|:----:|:----:| 190 | |trackMedia.enable|Boolean|この機能を使うか否か|`true`| 191 | |trackMedia.selector|String|観測対象のエレメントを検知するためのクエリーセレクター文字列|`video, audio`| 192 | |trackMedia.heartbeat|Integer|ユーザーがメディアを再生しているとき、ATJはここで指定するN秒ごとに「ハートビート=心拍」ビーコンを送信する|`5`| 193 | 194 | #### trackForm (オプション以下) 195 | 196 | |変数|型|目的|例| 197 | |:----:|:----:|:----:|:----:| 198 | |trackForm.enable|Boolean|この機能を使うか否か|`true`| 199 | |trackForm.target|Element|状態を追跡するフォーム要素のを内包する親要素を渡す|`document.getElementById('form_element')`| 200 | 201 | - この機能はまた実験段階である。 202 | 203 | #### trackUnload (オプション以下) 204 | 205 | |変数|型|目的|例| 206 | |:----:|:----:|:----:|:----:| 207 | |trackUnload.enable|Boolean|この機能を使うか否か|`true`| 208 | 209 | - ATJはユーザーがunloadしようとした時にビーコンを送信するが、精度はエンドポイントとの接続性能、DNS、クライアントに依存する。希にブラウザによってリクエストが中断される。 210 | 211 | ## initPage() に対するページレベルの設定引数 212 | 213 | `initPage()` はページ固有の設定変数を内包する一つのオブジェクトを受け取る。 214 | ほとんどの変数は省略可能だが、明示的に値を指定することを強く推奨する。 215 | 216 | ### 基本構造 217 | 218 | ```javascript 219 | { 220 | user: {...}, 221 | context: {...} 222 | } 223 | ``` 224 | 225 | ### 変数 226 | 227 | #### user 228 | 229 | |変数|型|目的|例| 230 | |:----:|:----:|:----:|:----:| 231 | |user_id|String|サービス側で認識したユーザーID|`abc123`| 232 | |user_status|String|ログイン状態、権限、料金プランのようなユーザーのステータス|`loggedin`| 233 | |site_session|String|サービス側で扱われているセッションID。Atlasのデータとサーバーログを紐付ける場合、これがキーとなる|`ce6b2f45-5362-4aec-a1e0-e93474f6d898`| 234 | |external_ids|Map|Atlasのデータと他のシステムを統合するための付加的なユーザーID|`{thirdparty_tool: '987xyz'}`| 235 | 236 | - `user` はユーザーIDや環境などユーザー側の情報を格納するためのもの。 237 | - Atlas エンドポイントはセッション管理を内蔵しているので `site_session` はセッション管理に不要である。 238 | - `external_ids` は `initPage()` 以降に `setCustomId()` と `delCustomId()` で管理される。 239 | 240 | 241 | #### context 242 | 243 | |変数|型|目的|例| 244 | |:----:|:----:|:----:|:----:| 245 | |app|String|現在のページを担当しているアプリケーションやマイクロサービスの名称|`Hub`| 246 | |app_version|String|`app` に関して、アプリケーションやマイクロサービスのバージョン|`2.1.3`| 247 | |source|String|コンテンツがどこから提供されているか|`Nikkei`| 248 | |edition|String|コンテンツのエディション|`Online`| 249 | |content_id|String|コンテンツを識別するための一意な値|`abc123`| 250 | |content_name|String|コンテンツの見出し|`Nikkei made Atlas public as an opensource software`| 251 | |content_status|String|ペイウォールやメーターシステムを持つサービスの場合、コンテンツの可視性について全文表示中(`opened`)や一部表示(`locked`)などをセットできる|`opened`| 252 | |page_name|String|ページに対するページ名だが、コンテンツの見出しではない|`article`| 253 | |page_num|Integer|ページ番号|`atlasTracking.getQueryValue('page') `|| 1`| 254 | |category_l1|String|カテゴリー名。L1はコンテンツの大きなグループ|`Shoes`| 255 | |category_l2|String|カテゴリー名。L2はコンテンツの中規模のグループ|`Casual Shoes`| 256 | |category_l3|String|カテゴリー名。L3はコンテンツの細かいグループ|`Sneakers`| 257 | |tracking_code|String|AA互換のトラッキングコード用変数|`atlasTracking.getQueryValue('cid')`| 258 | |events|String|ページ上での購入(`purchase`)や送信(`submit`)、完了(`complete`)… といったイベントを指定できる|`purchase`| 259 | |campaign.name|String|GA互換のトラッキングパラメーター用変数|`atlasTracking.getQueryValue('utm_campaign')`| 260 | |campaign.source|String|GA互換のトラッキングパラメーター用変数|`atlasTracking.getQueryValue('utm_source')`| 261 | |campaign.medium|String|GA互換のトラッキングパラメーター用変数|`atlasTracking.getQueryValue('utm_medium')`| 262 | |campaign.term|String|GA互換のトラッキングパラメーター用変数|`atlasTracking.getQueryValue('utm_term')`| 263 | |campaign.content|String|GA互換のトラッキングパラメーター用変数|`atlasTracking.getQueryValue('utm_content')`| 264 | |search.term|String|サイト内検索に用いられたキーワード|`atlasTracking.getQueryValue('keyword')`| 265 | |search.options|Map|サイト内検索で適用された検索オプション|`{Region:'Asia',Limit:20}`| 266 | |search.results|Integer|サイト内検索の結果の件数|`document.getElementsByClassName('result-item').length`| 267 | |funnel.funnel_name|String|フォームの名前|`Subscription`| 268 | |funnel.funnel_steps|Integer|フォーム内の総ステップ数|`4`| 269 | |funnel.step_name|String|現在のフォームのステップの分かりやすい名前|`Confirmation`| 270 | |funnel.step_number|Integer|現在のフォームのステップの番号|`3`| 271 | |funnel.step_state|String|現在のステップの状態。例えば、ユーザーが誤った値を入力した場合、確認で失敗するので「failed」の状態となる|`success`| 272 | |funnel.products|Map|(実験段階)|`{}`| 273 | |flags|Map|特にサービスやユーザーについてのフラグデータのための任意のデータオブジェクト|`{ab_test_target:true}`| 274 | |custom_object|Map|事前定義済みではない任意のデータオブジェクト|`{foo:'bar'}`| 275 | 276 | - `context` は、コンテンツ名やコンテンツのカテゴリのようなコンテンツやサーバー側の情報を格納する。 277 | - `custom_object` は `initPage()` 以降に `setCustomObject()` と `delCustomObject()` で管理される。 278 | - `context` 直下の変数は `setCustomVars()` と `delCustomVars()` で追加/削除できる。 279 | -------------------------------------------------------------------------------- /docs/CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | # Configuration Guide 2 | 3 | ## Library Level Configuration Parameters for config() 4 | 5 | `config()` accepts one map object including configuration variables for ATJ. 6 | Most variables except `system` can be omitted but strongly recommend to specify values explicitly. 7 | 8 | ### Basic structure 9 | 10 | ```javascript 11 | { 12 | 'system': {...}, 13 | 'defaults': {...}, 14 | 'product': {...}, 15 | 'options': { 16 | 'exchangeAtlasId': {...}, 17 | 'trackClick': {...}, 18 | 'trackLink': {...}, 19 | 'trackDownload': {...}, 20 | 'trackNavigation': {...}, 21 | 'trackPerformance': {...}, 22 | 'trackScroll': {...}, 23 | 'trackInfinityScroll': {...}, 24 | 'trackRead': {...}, 25 | 'trackViewability': {...}, 26 | 'trackMedia': {...}, 27 | 'trackForm': {...}, 28 | 'trackUnload': {...} 29 | } 30 | } 31 | ``` 32 | 33 | ### Variables 34 | 35 | #### system 36 | 37 | |Variable|Type|Purpose|Example| 38 | |:----:|:----:|:----:|:----:| 39 | |endpoint|String|Destination which ATJ transmit beacons to|`atlas-endpoint.your.domain`| 40 | |apiKey|String|Atlas Endpoint will accept beacons having this key|`abc123xyz789`| 41 | |beaconTimeout|Integer|Time limit in milli sec to cancel the connection to the endpoint |`2000` (2 sec)| 42 | |cookieName|String| **DEPRECATED** Cookie name to store Atlas ID|`atlasId`| 43 | |cookieMaxAge|Integer| **DEPRECATED** Atlas ID Cookie lifetime|`(2 * 365 * 24 * 60 * 60)` (2 years)| 44 | |cookieDomain|String| **DEPRECATED** Domain to be used as domain-attribute when ATJ set Atlas ID Cookie|`your.domain`| 45 | |targetWindow|String|A name of (relative) target window where ATJ work in|`parent`| 46 | 47 | #### defaults 48 | 49 | |Variable|Type|Purpose|Example| 50 | |:----:|:----:|:----:|:----:| 51 | |pageUrl|String|Page URL where pageview and other events occurred at|`window.parent.document.location.href`| 52 | |pageReferrer|String|Referring URL = a previous page URL|`window.parent.document.referrer`| 53 | |pageTitle|String|Page title|`window.parent.document.title`| 54 | 55 | - Page title is friendly to identify the page but it requires you to set the different page title to each page. 56 | 57 | #### product 58 | 59 | |Variable|Type|Purpose|Example| 60 | |:----:|:----:|:----:|:----:| 61 | |productFamily|String|Product Family. If you have multiple UIs or applications to access your service, you can make these grouped by this value|`MyDigitalService`| 62 | |productName|String|Product Name. If you have multiple services within the same brand, you can identify each service by this value|`MyDigitalService-Web`| 63 | 64 | - You can set the same value to both variable if you have only one product under the brand. 65 | 66 | #### useGet (under options) 67 | 68 | - This feature was available until 2.14.2, is no longer available in newer version. 69 | 70 | |Variable|Type|Purpose|Example| 71 | |:----:|:----:|:----:|:----:| 72 | |useGet|Boolean|Switch the method to send beacons. `true` = GET, `false` = POST|`true`| 73 | 74 | - Some Japan specific proxy based security software removes a POST body but retain Content-Length header in the request. So, GET is safer ways to prevent data destruction caused by the security tool. 75 | 76 | #### exchangeAtlasId (under options) 77 | 78 | - This feature was available until 2.14.2, is no longer available in newer version. 79 | 80 | |Variable|Type|Purpose|Example| 81 | |:----:|:----:|:----:|:----:| 82 | |exchangeAtlasId.pass|Boolean|Use this feature or not|`true`| 83 | |exchangeAtlasId.passParamKey|String|Name of GET parameter to set Atlas ID into|`atlas_id`| 84 | |exchangeAtlasId.passTargetDomains|Array|Array of domains to pass Atlas ID|`['domain1','domain2','domain3']`| 85 | |exchangeAtlasId.catch|Boolean|Use this feature or not|`true`| 86 | |exchangeAtlasId.catchParamKey|String|Name of GET parameter to receive Atlas ID from|`atlas_id`| 87 | |exchangeAtlasId.catchTargetDomains|Array|Array of domains to receive Atlas ID|`['domain1','domain2','domain3']`| 88 | 89 | - ATJ can share Atlas ID across multiple domains without third party cookie and it uses GET parameter to exchange Atlas ID. 90 | - `pass` side is a feature to hand-over Atlas ID to external domains listed in `passTargetDomains` 91 | - `catch` side is a feature to receive Atlas ID passed from external domains listed in `catchTargetDomains` 92 | 93 | #### trackClick (under options) 94 | 95 | |Variable|Type|Purpose|Example| 96 | |:----:|:----:|:----:|:----:| 97 | |trackClick.enable|Boolean|Use this feature or not|`true`| 98 | |trackClick.targetAttribute|String|ATJ collects data when user clicked elements which has this data attribution |`data-atlas-trackable`| 99 | |trackClick.disableText|Boolean|If `true`, ATJ won't send a text on clicked element|`false`| 100 | |trackClick.logLastClick|Boolean|Inherit click information to the next page for Last Click feature|`true`| 101 | |trackClick.lastClickTtl|Integer|TTL in Sec about how long ATJ keep the last click information|`5`| 102 | |trackClick.useLastClickOnly|Boolean|If `true`, ATJ use only Last Click, and disable beacons on click event|`false`| 103 | |trackClick.logAllClicks|Boolean|If `true` then force to log all clicks. It allows both fully automated click tracking and naming events through data attribute|`true`| 104 | 105 | - If `enable` is `true` and `targetAttribute` has a value, elements with data-attribute specified as `targetAttribute` is tracked. 106 | - If `enable` is `true` but `targetAttribute` is not specified or has `false`, all clickable elements are tracked. 107 | - If `enable` is `true` and `targetAttribute`, but `logAllClicks` is available and set to `true`, all clicks will be tracked with custome naming by data attribute specified as `targetAttribute`. 108 | - In case `logLastClick` is enabled, a JSON string with key name `atlasLastClick` is added to Session Storage. (this item will be deleted on next page) 109 | 110 | #### trackLink (under options) 111 | 112 | |Variable|Type|Purpose|Example| 113 | |:----:|:----:|:----:|:----:| 114 | |trackLink.enable|Boolean|Use this feature or not|`true`| 115 | |trackLink.internalDomains|Array|Array of domains you want to exclude from the exit link clicks|`['domain1','domain2','domain3']`| 116 | |trackLink.nameAttribute|String|You can set a custom name for the link by adding data attribution and specify the attribution name here|`data-atlas-link-name`| 117 | 118 | #### trackDownload (under options) 119 | 120 | |Variable|Type|Purpose|Example| 121 | |:----:|:----:|:----:|:----:| 122 | |trackDownload.enable|Boolean|Use this feature or not|`true`| 123 | |trackDownload.fileExtensions|Array|Array of file extensions you want to track downloads|`['pdf','zip','tar','gz']`| 124 | |trackDownload.nameAttribute|String|You can set an alternative name for the file by adding data attribution and specify the attribution name here|`data-atlas-link-name`| 125 | 126 | 127 | #### trackNavigation (under options) 128 | 129 | |Variable|Type|Purpose|Example| 130 | |:----:|:----:|:----:|:----:| 131 | |trackNavigation.enable|Boolean|Use this feature or not|`true`| 132 | 133 | 134 | #### trackPerformance (under options) 135 | 136 | |Variable|Type|Purpose|Example| 137 | |:----:|:----:|:----:|:----:| 138 | |trackPerformance.enable|Boolean|Use this feature or not|`true`| 139 | 140 | - Track Performance helps you to monitor real-user performance information but the data object is little bit large. 141 | - If you want to measure Sec until onload, you may need to call `trackAction()` on the onload event due to load sequence. 142 | 143 | #### trackScroll (under options) 144 | 145 | |Variable|Type|Purpose|Example| 146 | |:----:|:----:|:----:|:----:| 147 | |trackScroll.enable|Boolean|Use this feature or not|`true`| 148 | |trackScroll.granularity|Integer|ATJ sends beacons when the scroll depth is changed more than N percent specified here (1-99)|`20`| 149 | |trackScroll.threshold|Integer|ATJ sends beacons if user keeps the scroll depth for over T sec defined here|`2`| 150 | 151 | - Scroll Depth is measured by the combination of both `granularity` and `threshold`. So, if user scroll-down to 90% but scroll-up to 10% within 1sec, ATJ doesn't send beacons. 152 | - Scroll Depth tracking only observes scroll behavior to the bottom side. 153 | - If your web site uses infinity scroll / lazy load, the standard scroll depth tracking doesn't fit. So, use `trackInfinityScroll` instead of `trackScroll`. 154 | 155 | #### trackInfinityScroll (under options) 156 | 157 | |Variable|Type|Purpose|Example| 158 | |:----:|:----:|:----:|:----:| 159 | |trackInfinityScroll.enable|Boolean|Use this feature or not|`true`| 160 | |trackInfinityScroll.step|Integer|If the scroll depth is changed more than N pixels/points specified here, ATJ sends beacons|`600`| 161 | |trackInfinityScroll.threshold|Integer|ATJ sends beacons if user keeps the scroll depth for over T sec defined here|`2`| 162 | 163 | #### trackRead (under options) 164 | 165 | |Variable|Type|Purpose|Example| 166 | |:----:|:----:|:----:|:----:| 167 | |trackRead.enable|Boolean|Use this feature or not|`true`| 168 | |trackRead.target|Element|Target element to be observed|`document.getElementById('article_body')`| 169 | |trackRead.granularity|Integer|ATJ sends beacons when the read-through rate is changed more than N percent specified here (1-99)|`25`| 170 | |trackRead.milestones|Array|ATJ also sends beacons when time elapsed more than these milestones|`[4, 15, 30, 60, 90, 120]`| 171 | 172 | - A difference between `trackScroll` and `trackRead` is: 173 | - `trackScroll` measures scroll depth of window 174 | - `trackRead` only focuses on visibility change on block element of content body 175 | - `trackScroll` works based on the combination of depth and time but `trackRead` uses depth and time separately. 176 | 177 | #### trackViewability (under options) 178 | 179 | |Variable|Type|Purpose|Example| 180 | |:----:|:----:|:----:|:----:| 181 | |trackViewability.enable|Boolean|Use this feature or not|`true`| 182 | |trackViewability.targets|Array|Array of HTML elements to be observed|`document.getElementsByClassName('ad_frame')`| 183 | 184 | - IAB's definition for Viewable Impression is applied but ATJ doesn't measure the exact seconds due to performance perspective (throttled every 250ms) 185 | 186 | #### trackMedia (under options) 187 | 188 | |Variable|Type|Purpose|Example| 189 | |:----:|:----:|:----:|:----:| 190 | |trackMedia.enable|Boolean|Use this feature or not|`true`| 191 | |trackMedia.selector|String|A query selector string to detect elements to be observed|`video, audio`| 192 | |trackMedia.heartbeat|Integer|When user playing media, ATJ sends "heartbeat" beacons every N seconds specified here|`5`| 193 | 194 | #### trackForm (under options) 195 | 196 | |Variable|Type|Purpose|Example| 197 | |:----:|:----:|:----:|:----:| 198 | |trackForm.enable|Boolean|Use this feature or not|`true`| 199 | |trackForm.target|Element|Pass the parent element which contains form elements to be tracked status|`document.getElementById('form_element')`| 200 | 201 | - This feature is still experimental. 202 | 203 | #### trackUnload (under options) 204 | 205 | |Variable|Type|Purpose|Example| 206 | |:----:|:----:|:----:|:----:| 207 | |trackUnload.enable|Boolean|Use this feature or not|`true`| 208 | 209 | - ATJ can send a beacon when user is about to unload the page, but the accuracy depends on the connection performance to the endpoint, DNS and client. Sometime the request is killed by browser. 210 | 211 | ## Page Level Configuration Parameters for initPage() 212 | 213 | `initPage()` accepts one map object including configuration variables for specific page. 214 | Most variables can be omitted but it will be helpful for you if you set concrete values. 215 | 216 | ### Basic structure 217 | 218 | ```javascript 219 | { 220 | user: {...}, 221 | context: {...} 222 | } 223 | ``` 224 | 225 | ### Variables 226 | 227 | #### user 228 | 229 | |Variable|Type|Purpose|Example| 230 | |:----:|:----:|:----:|:----:| 231 | |user_id|String|User ID recognized by service side|`abc123`| 232 | |user_status|String|User status like Login Status, Permission or Payment Plan|`loggedin`| 233 | |site_session|String|Session ID handled by service side. If you want to combine Atlas data and server logs, this can be used as a key|`ce6b2f45-5362-4aec-a1e0-e93474f6d898`| 234 | |external_ids|Map|Additional user IDs to integrate Atlas data and other systems|`{thirdparty_tool: '987xyz'}`| 235 | 236 | - `user` is to store user side information such as user ID and environment. 237 | - Atlas Endpoint has built-in session management so `site_session` is not necessary to handle sessions. 238 | - `external_ids` can be managed by `setCustomId()` and `delCustomId()` after `initPage()` 239 | 240 | 241 | #### context 242 | 243 | |Variable|Type|Purpose|Example| 244 | |:----:|:----:|:----:|:----:| 245 | |app|String|Name of application or micro-service responsible for the current page|`Hub`| 246 | |app_version|String|Version of application or micro-service. Related to `app` var|`2.1.3`| 247 | |source|String|Where content provided from|`Nikkei`| 248 | |edition|String|Edition of content|`Online`| 249 | |content_id|String|Unique value for identifying specific content|`abc123`| 250 | |content_name|String|Title of content|`Nikkei made Atlas public as an opensource software`| 251 | |content_status|String|If your service has a paywall or a meter system, you can set the status of content visibility (like `opened`,`locked`)|`opened`| 252 | |page_name|String|Pagename for the page but not content name|`article`| 253 | |page_num|Integer|Pagenation|`atlasTracking.getQueryValue('page') `|| 1`| 254 | |category_l1|String|Category name. L1 is for large group of content|`Shoes`| 255 | |category_l2|String|Category name. L2 is for medium size group of content|`Casual Shoes`| 256 | |category_l3|String|Category name. L3 is for small group of content|`Sneakers`| 257 | |tracking_code|String|Variable for AA compatible tracking code|`atlasTracking.getQueryValue('cid')`| 258 | |events|String|You can specify an event on the page such as `purchase`, `submit` or `complete`...|`purchase`| 259 | |campaign.name|String|Variable for GA compatible tracking parameters|`atlasTracking.getQueryValue('utm_campaign')`| 260 | |campaign.source|String|Variable for GA compatible tracking parameters|`atlasTracking.getQueryValue('utm_source')`| 261 | |campaign.medium|String|Variable for GA compatible tracking parameters|`atlasTracking.getQueryValue('utm_medium')`| 262 | |campaign.term|String|Variable for GA compatible tracking parameters|`atlasTracking.getQueryValue('utm_term')`| 263 | |campaign.content|String|Variable for GA compatible tracking parameters|`atlasTracking.getQueryValue('utm_content')`| 264 | |search.term|String|Keyword which is searched in site search|`atlasTracking.getQueryValue('keyword')`| 265 | |search.options|Map|Search options applied to site search|`{Region:'Asia',Limit:20}`| 266 | |search.results|Integer|Number of result of site search|`document.getElementsByClassName('result-item').length`| 267 | |funnel.funnel_name|String|Name of thr form|`Subscription`| 268 | |funnel.funnel_steps|Integer|Total steps in the form|`4`| 269 | |funnel.step_name|String|Current step of the form in user friendly name|`Confirmation`| 270 | |funnel.step_number|Integer|Current step of the form in number|`3`| 271 | |funnel.step_state|String|State of current step. For example, if user filled wrong value into the form, the confirmation process failed and the value must be 'failed'|`success`| 272 | |funnel.products|Map|(experimental)|`{}`| 273 | |flags|Map|Custom data object especially for flag data of service or user|`{ab_test_target:true}`| 274 | |custom_object|Map|Custom data object other than pre-defined variables|`{foo:'bar'}`| 275 | 276 | - `context` is to store contents or server side information such as content name, category of content, campaign and conversion related things. 277 | - `custom_object` can be managed by `setCustomObject()` and `delCustomObject()` after `initPage()` 278 | - You can add/delete variables just under `context` by using `setCustomVars()` and `delCustomVars()` 279 | -------------------------------------------------------------------------------- /docs/CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Atlas Tracking JS contributors 2 | 3 | ## Developers 4 | 5 | - [Hajime Sano](https://github.com/hjmsano) 6 | - v1 and v2 development 7 | - Current maintainer 8 | 9 | - [komlow](https://github.com/komlow) 10 | - Test code preparation & Lint optimization 11 | - Current maintainer 12 | 13 | ## Supporters 14 | 15 | - [Andrew Betts](https://github.com/triblondon) 16 | - Thank you for your help on Data Transmission Mechanism especially use of `sendBeacon` 17 | 18 | 19 | - [ysugimoto](https://github.com/ysugimoto) 20 | - Thank you for the first refactoring and your idea to use `requestAnimationFrame` for the recurring observation event. 21 | 22 | ## Full contributors list 23 | 24 | Big appreciation for [everyone who contributed](https://github.com/Nikkei/atlas-tracking-js/graphs/contributors) improvement of Atlas Tracking JS. 25 | -------------------------------------------------------------------------------- /docs/FAQ-JP.md: -------------------------------------------------------------------------------- 1 | # よくある質問 2 | 3 | ## 実装 4 | 5 | ### 特定のリンクのクリック数を計測するには? 6 | 7 | いくつかの選択肢がある。 8 | 9 | 1. 計測対象の `a`、 `button` または `input` タグにデータ属性を追加する。デフォルトの属性名は `data-atlas-trackable` だが、 `config()` 内の `trackClick.targetAttribute` オプションでターゲット要素の検出に使う属性名を変更できる。 10 | 2. 対象要素上のクリックを検出するイベントリスナーを作成し、`trackAction()` を呼ぶ。 11 | 3. `onClick` 属性を対象要素に追加し、 `trackAction()` を呼ぶ。 12 | 13 | 1が簡単で安全なのでベストな方法である。(ユーザーが、Atlas Trackingの初期化前にリンクをクリックしたらどうなる?) 14 | ほとんどのサードパーティツールは2または3をドキュメントの中で推奨しているが、3は読み込み順序の制御が難しい。 15 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## Implementation 4 | 5 | ### How can I measure clicks on a specific link? 6 | 7 | There are three choice. 8 | 9 | 1. Add a data attribute to `a`, `button` or `input` tags which is a target to be tracked. Default attribution name is `data-atlas-trackable` but you can change the name for detecting the target element through `trackClick.targetAttribute` option in `config()`. 10 | 2. Create an event listener to detect clicks on the target element, and call `trackAction()` from the listener. 11 | 3. Add `onClick` attribute to the target element and call `trackAction()`. 12 | 13 | 1 is the best way because it's easy and safe. (Imagine if an user clicks the link before Atlas Tracking is initialized?) 14 | Most 3rd party tools recommend 2 or 3 in their document but 3 is hard to control the loading sequence. -------------------------------------------------------------------------------- /docs/METHODS-JP.md: -------------------------------------------------------------------------------- 1 | # 関数一覧 2 | 3 | ## 基本 4 | 5 | ### config(object) 6 | 7 | ATJコアを設定する。 8 | 詳細は [設定ガイド](./CONFIGURATION-JP.md#config-に対するライブラリレベルの設定引数) を参照 9 | 10 | - 引数 11 | - object (map : ATJコアレベルの設定情報を含むオブジェクト) 12 | - 戻値 13 | - void 14 | 15 | 16 | ### initPage(object) 17 | 18 | ページに対する変数とイベントリスナーを初期化する。 19 | 詳細は [設定ガイド](./CONFIGURATION-JP.md#initpage-に対するページレベルの設定引数) を参照 20 | 21 | - 引数 22 | - object (map : ページに対する設定情報を含むオブジェクト) 23 | - 戻値 24 | - void 25 | 26 | 27 | ### trackPage() 28 | 29 | ページビューイベントを送信する。 30 | 31 | - 引数 32 | - void 33 | - 戻値 34 | - void 35 | 36 | 37 | ### trackAction(action, category, events, object) 38 | 39 | 任意のイベントを送信する。 40 | `action` と `category` を指定することを強く推奨する。また、action と category は、粗い粒度であるべき。 41 | 42 | 例えば、もし「お気に入りアイテムに保存」ボタンのクリック数を計りたい場合: 43 | 44 | |評価|Action|Category|Object|メモ| 45 | |:----:|:----:|:----:|:----:|:----:| 46 | |良|`click`|`button`|`{loc:'header',fnc:'save',tgt:'favorite-items'}`|第4引数に機能や場所を指すコンテキストを追加する| 47 | |悪|`click-to-save`|`favorite-items-header-button`|`{}`|これは複数の組み合わせパターンに対して「総ボタンクリック数」を集計したい場合に問題となり得る| 48 | 49 | - 引数 50 | - action (string : 任意。アクションを説明する動詞または動名詞。例: `click`, `open`, `receive`) 51 | - category (string : 任意。アクションが適用される対象を指す名詞または目的語。例: `button`, `link`, `notification`) 52 | - events (string : 任意。アクションに対してイベント名を振りたい場合、カンマ区切りまたは単体のイベント名) 53 | - object (map : 任意。任意の変数を含むオブジェクト) 54 | - 戻値 55 | - void 56 | 57 | 58 | ## ATJ外部からデータを取得する 59 | 60 | ### getQueryValue(keyName) 61 | 62 | 現URLのGETパラメーターをパースして指定したキーの値を得る。 63 | 64 | - 引数 65 | - keyName (string : キー名) 66 | - 戻値 67 | - GETパラメータの {keyName} が持つ値 68 | 69 | ```javascript 70 | // 「cid」の値を取得してtracking_codeにセットする 71 | context.tracking_code = atlasTracking.getQueryValue('cid'); 72 | ``` 73 | 74 | 75 | ### getCookieValue(keyName) 76 | 77 | ブラウザのCookieをパースし、指定したキーの値を得る。 78 | 79 | - 引数 80 | - keyName (string : キー名) 81 | - 戻値 82 | - Cookieの {keyName} が持つ値 83 | 84 | ```javascript 85 | // 「uid」の値を取得してuser_idにセットする 86 | user.user_id = atlasTracking.getCookieValue('uid'); 87 | ``` 88 | 89 | 90 | ### getLocalStorageValue(keyName) 91 | 92 | ブラウザのLocal Storageから指定したキーの値を得る。 93 | 94 | - 引数 95 | - keyName (string : キー名) 96 | - 戻値 97 | - Local Storageの {keyName} が持つ値 98 | 99 | ```javascript 100 | // 「flags」の値を取得し、パースしてflagsにセットする 101 | context.flags = JSON.parse(atlasTracking.getLocalStorageValue('flags')); 102 | ``` 103 | 104 | ### setDataSrc(jsonString) 105 | 106 | ATJの外側から供給されるカスタムデータオブジェクトをパースし、ATJ内の一時変数に格納する。 107 | 108 | - 引数 109 | - jsonString (string : StringifyされたJSON文字列) 110 | - 戻値 111 | - パース結果のMapオブジェクト 112 | 113 | ```javascript 114 | // GETパラメーターを通じてカスタムオブジェクトをATJに供給する 115 | atlasTracking.setDataSrc(atlasTracking.getQueryValue('custom_data')); 116 | ``` 117 | 118 | 119 | ### getDataFromSrc(mapPath) 120 | 121 | JSONパスを指定することで、 `setDataSrc()` でパースされたカスタムオブジェクトから指定の値を取り出す。 122 | 123 | - 引数 124 | - mapPath (string : 取り出したい値までのパス) 125 | - 戻値 126 | - DataSrcの一時変数から取り出した、{mapPath} が持つ値 127 | 128 | ```javascript 129 | // ページ名を外部から供給されるDataSrcから取得してpage_nameにセットする 130 | context.page_name = atlasTracking.getDataFromSrc('page_data.name'); 131 | ``` 132 | 133 | 134 | ## Ingest内部の値を操作する 135 | 136 | ### setCustomObject(keyName, object) 137 | 138 | キー・バリューの組み合わせをカスタムデータを格納するためのcustom_objectに追加する。 139 | custom_objectは64kbまでの大きなオブジェクトを格納できるが、1件のビーコンの合計サイズに配慮すること。 140 | 141 | - 引数 142 | - keyName (string : 追加する値を格納するキー名) 143 | - object (String, Number, Map, Array : 格納する任意の値) 144 | - 戻値 145 | - void 146 | 147 | ```javascript 148 | // A/Bテストの詳細をCustom Objectに追加 149 | atlasTracking.setCustomObject('ab_testing', {target_user:true,pattern_name:'onboarding_cp',creative_id:'benefit_offering_001'}); 150 | ``` 151 | 152 | ### delCustomObject(keyName) 153 | 154 | keyNameで指定された特定のデータを custom_objectから削除する。 155 | 156 | - 引数 157 | - keyName (string : 削除されるキー名) 158 | - 戻値 159 | - void 160 | 161 | ```javascript 162 | // A/Bテストの詳細をCustom Objectから削除する 163 | atlasTracking.delCustomVars('ab_testing'); 164 | ``` 165 | 166 | 167 | ### setCustomVars(keyName, object) 168 | 169 | Context直下に任意の変数を追加する。 170 | 171 | - 引数 172 | - keyName (string : 追加する値を格納するキー名) 173 | - object (String, Number, Map, Array : 格納する任意の値) 174 | - 戻値 175 | - void 176 | 177 | ```javascript 178 | // 「flags」をグローバル変数「analytics_data」からセットする 179 | atlasTracking.setCustomVars('flags', window.analytics_data); 180 | ``` 181 | 182 | ### delCustomVars(keyName) 183 | 184 | 指定したキー名の値をContext直下から削除する。 185 | 186 | - 引数 187 | - keyName (string : 削除するキー名) 188 | - 戻値 189 | - void 190 | 191 | ```javascript 192 | // 「flags」をContext直下から削除する 193 | atlasTracking.delCustomVars('flags'); 194 | ``` 195 | 196 | 197 | ### setCustomId(keyName, customID) 198 | 199 | 別のシステムでユーザーを識別するための情報を追加する。 200 | もしウェブサイト上で複数のツールを分析やマーケティングに利用している場合、複数のツールのデータをここで指定するカスタムIDをキーにして統合できる。 201 | 202 | - 引数 203 | - keyName (string : 追加する値を格納するキー名) 204 | - customID (string : カスタムID) 205 | - 戻値 206 | - void 207 | 208 | ```javascript 209 | // RtoasterのIDを user.external_ids に追加する 210 | atlasTracking.setCustomId('rtoaster', 'abc123'); 211 | ``` 212 | 213 | 214 | ### delCustomId(keyName) 215 | 216 | カスタムIDを削除する。 217 | 218 | - 引数 219 | - keyName (string : 削除するキー名) 220 | - 戻値 221 | - void 222 | 223 | ```javascript 224 | // RtoasterのIDを user.external_ids から削除する 225 | atlasTracking.delCustomId('rtoaster'); 226 | ``` 227 | 228 | 229 | ## Other useful functions 230 | 231 | ### initEventListeners() 232 | 233 | イベントリスナーを再初期化する。 `initPage()` の後にページ内の要素が変更される場合、このメソッドを呼ぶことでATJ内部で利用するイベントリスナーをリセットできる。 234 | ただし、ATJ内でイベントリスナーを活用するほとんどの仕組みはイベントデリゲーションが適用されているため、イベントリスナーのリセットの必要性は少ない。 235 | 236 | - 引数 237 | - void 238 | - 戻値 239 | - void 240 | 241 | ### getVisibility(htmlElement) 242 | `getVisibility()` は指定した特定のHTML要素の可視性を評価し結果を返す。 243 | 244 | - 引数 245 | - htmlElement (element : 評価対象のエレメント単体) 246 | - 戻値 247 | - result (map : 以下に説明する可視性についての様々な情報) 248 | 249 | |Path|Type|Meaning|Example| 250 | |:----:|:----:|:----:|:----:| 251 | |status.isInView|Boolean|指定要素の一部または全体問わず見えるか否か|`true`| 252 | |status.location|String|all、top、bottomのような大まかな見えている部位|`all`| 253 | |detail.documentHeight|Float|documentの高さのピクセルまたはポイント(iOS)|`4401`| 254 | |detail.documentIsVisible|String|documentの可視性。もしタブがアクティブであれば `visible` 、バックグラウンドであれば `hidden`|`visible`| 255 | |detail.documentScrollUntil|Float|windowにおける可視領域の下端の位置|`894`| 256 | |detail.documentScrollRate|Float|scrollUntil と documentHeight を比較したスクロール率|`0.203135665`| 257 | |detail.documentVisibleTop|Float|documentの可視領域の上端位置|`735`| 258 | |detail.documentVisibleBottom|Float|documentの可視領域の下端位置|`894`| 259 | |detail.targetHeight|Float|対象要素の高さのピクセルまたはポイント(iOS)|`269`| 260 | |detail.targetMarginTop|Float|viewportの上端から対象要素の上端までの距離|`455.03125`| 261 | |detail.targetMarginBottom|Float|viewportの下端から対象要素の下端までの距離|`169.96875`| 262 | |detail.targetScrollRate|Float|対象要素に対する可視領域のスクロール率。1の場合は対象要素の下端まで見えているという意味|`1`| 263 | |detail.targetScrollUntil|Float|対象要素のスクロール深度|`269`| 264 | |detail.targetViewableRate|Float|対象要素の可視領域の割合|`1`| 265 | |detail.targetVisibleTop|Float|対象要素の可視領域の上端位置|`0`| 266 | |detail.targetVisibleBottom|Float|対象要素の可視領域の下端位置|`269`| 267 | |detail.viewportHeight|Float|Viewport(ブラウザウィンドウ内側)の高さ|`894`| 268 | -------------------------------------------------------------------------------- /docs/METHODS.md: -------------------------------------------------------------------------------- 1 | # List of Methods 2 | 3 | ## Fundamentals 4 | 5 | ### config(object) 6 | 7 | Configure ATJ core. 8 | For more detail, read [Configuration Guide](./CONFIGURATION.md#library-level-configuration-parameters-for-config). 9 | 10 | - Parameters 11 | - object (map : an object containing the ATJ core level configurations) 12 | - Return 13 | - void 14 | 15 | 16 | ### initPage(object) 17 | 18 | Initialize page level variables and event listeners. 19 | For more detail, read [Configuration Guide](./CONFIGURATION.md#page-level-configuration-parameters-for-initpage). 20 | 21 | - Parameters 22 | - object (map : an object containing the page level configurations) 23 | - Return 24 | - void 25 | 26 | 27 | ### trackPage() 28 | 29 | Send a beacon as a pageview. 30 | This method automatically generate an action with `action=view` and `category=page`. 31 | 32 | - Parameters 33 | - void 34 | - Return 35 | - void 36 | 37 | 38 | ### trackAction(action, category, events, object) 39 | 40 | Send a beacon as a custom action. 41 | Strongly recommend to specify `action` and `category`. Also action and category must be coarse granularity. 42 | 43 | For example, if you want to measure clicks on "save to favorite items" button in the header of the page: 44 | 45 | |Valuation|Action|Category|Object|Note| 46 | |:----:|:----:|:----:|:----:|:----:| 47 | |Good|`click`|`button`|`{loc:'header',fnc:'save',tgt:'favorite-items'}`|Add some context into the fourth parameter to identify the button feature and the location| 48 | |Bad|`click-to-save`|`favorite-items-header-button`|`{}`|This could be a problem when you calculate "total clicks on all buttons" by combining multiple patterns| 49 | 50 | - Parameters 51 | - action (string : optional.a verb or a verbal noun which describes the action. ex. `click`, `open`, `receive`) 52 | - category (string : optional.a noun or a object which means the target to be applied the action to. ex. `button`, `link`, `notification`) 53 | - events (string : optional.a comma separated event names or single event name if you want to set. ex. `purchase`) 54 | - object (map : optional.an object including custom variables) 55 | - Return 56 | - void 57 | 58 | 59 | ## For retrieving values from outside of ATJ 60 | 61 | ### getQueryValue(keyName) 62 | 63 | Parse and get a value of specified key from GET query parameter in the current URL. 64 | 65 | - Parameters 66 | - keyName (string : a key name) 67 | - Return 68 | - A value of {keyName} in GET parameter 69 | 70 | ```javascript 71 | // Retrieve a value of "cid" and set it as tracking_code 72 | context.tracking_code = atlasTracking.getQueryValue('cid'); 73 | ``` 74 | 75 | 76 | ### getCookieValue(keyName) 77 | 78 | Parse and get a value of specified key from Cookies of the browser. 79 | 80 | - Parameters 81 | - keyName (string : a key name) 82 | - Return 83 | - A value of {keyName} in Cookies 84 | 85 | ```javascript 86 | // Retrieve a value of "uid" and set it as user_id 87 | user.user_id = atlasTracking.getCookieValue('uid'); 88 | ``` 89 | 90 | 91 | ### getLocalStorageValue(keyName) 92 | 93 | Select a value of specified key from Local Storage of the browser. 94 | 95 | - Parameters 96 | - keyName (string : a key name) 97 | - Return 98 | - A value of {keyName} in Local Storage 99 | 100 | ```javascript 101 | // Retrieve a value of "flags" and parse and set it as flags 102 | context.flags = JSON.parse(atlasTracking.getLocalStorageValue('flags')); 103 | ``` 104 | 105 | ### setDataSrc(jsonString) 106 | 107 | Parse a custom data object sourced from outside of ATJ, and then store the data in a temporary variable in ATJ object. 108 | 109 | - Parameters 110 | - jsonString (string : a stringified JSON text) 111 | - Return 112 | - A map object based on jsonString 113 | 114 | ```javascript 115 | // Feed a custom data object to ATJ through GET parameter 116 | atlasTracking.setDataSrc(atlasTracking.getQueryValue('custom_data')); 117 | ``` 118 | 119 | 120 | ### getDataFromSrc(mapPath) 121 | 122 | Select a value from the custom object parsed by `setDataSrc()` by specifying a JSON path. 123 | 124 | - Parameters 125 | - mapPath (string : a path to the variable you want to select) 126 | - Return 127 | - A value of {mapPath} stored in the temporary variable of DataSrc 128 | 129 | ```javascript 130 | // Pick up and set a pagename from DataSrc which is provided from outside of ATJ 131 | context.page_name = atlasTracking.getDataFromSrc('page_data.name'); 132 | ``` 133 | 134 | 135 | ## For managing values in the ingest 136 | 137 | ### setCustomObject(keyName, object) 138 | 139 | Add a key-value pair into custom_object which is an object to store custom data. 140 | custom_object is able to contain a large object up to 64kb but need to consider with the total request size of single beacon. 141 | 142 | - Parameters 143 | - keyName (string : a name of key to be added) 144 | - object (String, Number, Map, Array : any value) 145 | - Return 146 | - void 147 | 148 | ```javascript 149 | // Set the detail of A/B testing into Custom Object 150 | atlasTracking.setCustomObject('ab_testing', {target_user:true,pattern_name:'onboarding_cp',creative_id:'benefit_offering_001'}); 151 | ``` 152 | 153 | ### delCustomObject(keyName) 154 | 155 | Remove the particular data specified by keyName from custom_object. 156 | 157 | - Parameters 158 | - keyName (string : a name of key to be removed) 159 | - Return 160 | - void 161 | 162 | ```javascript 163 | // Remove the detail of A/B testing from Custom Object 164 | atlasTracking.delCustomVars('ab_testing'); 165 | ``` 166 | 167 | 168 | ### setCustomVars(keyName, object) 169 | 170 | Add the custom variable to just under Context. 171 | 172 | - Parameters 173 | - keyName (string : a name of key to be added) 174 | - object (String, Number, Map, Array : any value) 175 | - Return 176 | - void 177 | 178 | ```javascript 179 | // Set "flags" from the global object named "analytics_data" 180 | atlasTracking.setCustomVars('flags', window.analytics_data); 181 | ``` 182 | 183 | ### delCustomVars(keyName) 184 | 185 | Remove the particular variable specified by keyName from just under Context. 186 | 187 | - Parameters 188 | - keyName (string : a name of key to be removed) 189 | - Return 190 | - void 191 | 192 | ```javascript 193 | // Remove the variable named "flags" from Context 194 | atlasTracking.delCustomVars('flags'); 195 | ``` 196 | 197 | 198 | ### setCustomId(keyName, customID) 199 | 200 | Add an alternative information to identify the user by different system. 201 | If you are using multiple tools to execute analytics and/or marketing on your web site, you can combine data across multiple tools by using custom ID as a key. 202 | 203 | - Parameters 204 | - keyName (string : a name of key to be added) 205 | - customID (string : custom ID) 206 | - Return 207 | - void 208 | 209 | ```javascript 210 | // Add Rtoaster's ID into user.external_ids 211 | atlasTracking.setCustomId('rtoaster', 'abc123'); 212 | ``` 213 | 214 | 215 | ### delCustomId(keyName) 216 | 217 | Remove the custom ID. 218 | 219 | - Parameters 220 | - keyName (string : a name of key to be removed) 221 | - Return 222 | - void 223 | 224 | ```javascript 225 | // Remove Rtoaster's ID from user.external_ids 226 | atlasTracking.delCustomId('rtoaster'); 227 | ``` 228 | 229 | 230 | ## Other useful functions 231 | 232 | ### initEventListeners() 233 | 234 | Re-initialize Event Listeners. If elements in the page changed since `initPage()`, you can reset all Event Listeners used in ATJ by calling this method. 235 | However, most mechanism performing Event Listener in ATJ applies Event Delegation, so there is few needs to reset Event Listeners. 236 | 237 | - Parameters 238 | - void 239 | - Return 240 | - void 241 | 242 | ### getVisibility(htmlElement) 243 | 244 | `getVisibility()` evaluates and returns information about visibility of the particular HTML element specified in the parameter. 245 | 246 | - Parameters 247 | - htmlElement (element : a single HTML element to be evaluated) 248 | - Return 249 | - result (map : a variety of information about visibility status described below) 250 | 251 | |Path|Type|Meaning|Example| 252 | |:----:|:----:|:----:|:----:| 253 | |status.isInView|Boolean|The target element is partially/totally viewable or not|`true`| 254 | |status.location|String|Rough description for visible part like all, top, bottom...|`all`| 255 | |detail.documentHeight|Float|The height of document in pixel or point(iOS)|`4401`| 256 | |detail.documentIsVisible|String|Visibility of the document. If the tab is active then `visible`, if the tab is background then `hidden`|`visible`| 257 | |detail.documentScrollUntil|Float|The location of bottom side in visible area of window|`894`| 258 | |detail.documentScrollRate|Float|The rate of scroll depth comparing between scrollUntil and documentHeight|`0.203135665`| 259 | |detail.documentVisibleTop|Float|The location of top side of visible area for document|`735`| 260 | |detail.documentVisibleBottom|Float|The location of bottom side of visible area for document|`894`| 261 | |detail.targetHeight|Float|The height of target element in pixel or point(iOS)|`269`| 262 | |detail.targetMarginTop|Float|Distance between the top of target element and the top of viewport|`455.03125`| 263 | |detail.targetMarginBottom|Float|Distance between the bottom of target element and the bottom of viewport|`169.96875`| 264 | |detail.targetScrollRate|Float|The rate of scroll depth of the target element. If 1, it means the element is visible from top until bottom|`1`| 265 | |detail.targetScrollUntil|Float|The scroll depth on target element|`269`| 266 | |detail.targetViewableRate|Float|Percentage of visible area of target element|`1`| 267 | |detail.targetVisibleTop|Float|The location of top side of visible area for target element|`0`| 268 | |detail.targetVisibleBottom|Float|The location of bottom side of visible area for target element|`269`| 269 | |detail.viewportHeight|Float|The height of viewport|`894`| 270 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals'; 3 | import esParser from '@babel/eslint-parser' 4 | export default [ 5 | js.configs.recommended, 6 | { 7 | files: ['src/*.js'], 8 | languageOptions: { 9 | sourceType: 'module', 10 | parser: esParser, 11 | parserOptions: { 12 | ecmaVersion: 6 13 | }, 14 | globals: { 15 | ...globals.browser, 16 | ...globals.process, 17 | ...globals.node, 18 | }, 19 | }, 20 | rules: { 21 | 'newline-before-return': 'error', 22 | 'no-console': 'off', 23 | 'no-var': 'error', 24 | indent: [ 25 | 'error', 26 | 4, 27 | { 28 | SwitchCase: 1, 29 | }, 30 | ], 31 | }, 32 | }, 33 | ] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atlas-tracking-js", 3 | "version": "2.16.10", 4 | "description": "ATJ: Atlas Tracking JS provides capabilities for measuring user activities on your website.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "export NODE_OPTIONS=--dns-result-order=ipv4first && pnpm run test:build && mocha-chrome ./test/test.html --no-colors --ignore-exceptions --chrome-launcher.maxConnectionRetries=100", 8 | "test:build": "webpack -m -c ./webpack.common.js -c ./webpack.dev.js", 9 | "build:npm": "rollup -c --bundleConfigAsCjs", 10 | "build:dist": "webpack -m -c ./webpack.common.js -c ./webpack.prd.js", 11 | "prepublishOnly": "pnpm run build:npm", 12 | "eslint": "eslint --fix ./src --ext .js" 13 | }, 14 | "author": "Nikkei Inc.", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@babel/core": "^7.26.9", 18 | "@babel/eslint-parser": "^7.26.8", 19 | "@babel/plugin-proposal-decorators": "^7.25.9", 20 | "@babel/preset-env": "^7.26.9", 21 | "@eslint/js": "^9.22.0", 22 | "babel-loader": "^10.0.0", 23 | "babel-plugin-transform-inline-environment-variables": "^0.4.4", 24 | "chai": "^5.2.0", 25 | "eslint": "^9.22.0", 26 | "globals": "^16.0.0", 27 | "mocha": "^11.1.0", 28 | "mocha-chrome": "^2.2.0", 29 | "rollup": "^4.35.0", 30 | "sinon": "^19.0.2", 31 | "webpack": "^5.98.0", 32 | "webpack-cli": "^6.0.1", 33 | "webpack-merge": "^6.0.1" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/Nikkei/atlas-tracking-js.git" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/Nikkei/atlas-tracking-js/issues" 41 | }, 42 | "homepage": "https://github.com/Nikkei/atlas-tracking-js#readme", 43 | "directories": { 44 | "test": "test" 45 | }, 46 | "pnpm": { 47 | "onlyBuiltDependencies": [ 48 | "core-js" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'src/index.js', 3 | output: { 4 | file: 'dist/index.js', 5 | format: 'cjs' 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/index.dist.js: -------------------------------------------------------------------------------- 1 | import AtlasTracking from './index.js'; 2 | const SDK_NAMESPACE = process.env.SDK_NAMESPACE || 'atlasTracking'; 3 | window[SDK_NAMESPACE] = new AtlasTracking(); 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Utils from './utils.js'; 4 | 5 | const visibilityEventName = 'atlasVisibilityStatus'; 6 | const lastClickStorageKey = 'atlasLastClick'; 7 | 8 | let system = {}; 9 | let options = {}; 10 | let user = {}; 11 | let context = {}; 12 | let dataSrc = {}; 13 | let defaults = {}; 14 | let supplement = {}; 15 | let performanceInfo = {}; 16 | let atlasDOMContentLoadedHandler = null; 17 | let targetWindow = window['parent']; 18 | let visibilityEvent = null; 19 | let unloadEvent = null; 20 | let eventHandlerKeys = { 21 | unload: null, 22 | scroll: null, 23 | infinityScroll: null, 24 | read: null, 25 | click: null, 26 | viewability: {}, 27 | media: {}, 28 | form: {} 29 | }; 30 | 31 | let isInitialized = false; 32 | let pageLoadedAt, prevActionOccurredAt; 33 | pageLoadedAt = prevActionOccurredAt = Date.now(); 34 | 35 | export default class AtlasTracking { 36 | constructor() { 37 | 38 | } 39 | 40 | /** 41 | * get current url's query value associated with argument. 42 | * @param {string} k key of the query parameters. 43 | * @return {string} value associated with the key. 44 | */ 45 | getQueryValue(k) { 46 | return this.utils.getQ(k) || ''; 47 | } 48 | 49 | /** 50 | * get current cookie value associated with argument. 51 | * @param {string} k key of the cookie value. 52 | * @return {string} value associated with the key. 53 | */ 54 | getCookieValue(k) { 55 | return this.utils.getC(k) || ''; 56 | } 57 | 58 | 59 | /** 60 | * get current localStorage value associated with argument. 61 | * @param {string} k key of the localStorage index key. 62 | * @return {string} value associated with the key. 63 | */ 64 | getLocalStorageValue(k) { 65 | return this.utils.getLS(k) || ''; 66 | } 67 | 68 | /** 69 | * store stringified JSON. 70 | * @param {string} s stringified JSON string. 71 | */ 72 | setDataSrc(s) { 73 | dataSrc = JSON.parse(decodeURIComponent(s)); 74 | } 75 | 76 | /** 77 | * get value from Object stored by `setDataSrc`. 78 | * @param {string} d key to fetch data. 79 | * @return {string|number|Object|Array|Boolean} fetched data. 80 | */ 81 | getDataFromSrc(d) { 82 | return dataSrc[d] || ''; 83 | } 84 | 85 | /** 86 | * Inspect the element's visibility 87 | * @param {HTMLElement} t element to be inspected. 88 | * @return {Object} inspection result. 89 | */ 90 | getVisibility(t) { 91 | return this.utils.getV(t) || ''; 92 | } 93 | 94 | /** 95 | * configure atlas tracking. 96 | * @param {Object} obj configuration object. 97 | */ 98 | config(obj) { 99 | 100 | system = obj.system !== void 0 ? obj.system : {}; 101 | targetWindow = system.targetWindow ? window[system.targetWindow] : window['parent']; 102 | defaults.url = obj.defaults.pageUrl !== void 0 ? obj.defaults.pageUrl : targetWindow.document.location.href; 103 | defaults.referrer = obj.defaults.pageReferrer !== void 0 ? obj.defaults.pageReferrer : targetWindow.document.referrer; 104 | defaults.page_title = obj.defaults.pageTitle !== void 0 ? obj.defaults.pageTitle : targetWindow.document.title; 105 | defaults.product_family = obj.product.productFamily !== void 0 ? obj.product.productFamily : null; 106 | defaults.product = obj.product.productName !== void 0 ? obj.product.productName : null; 107 | 108 | this.utils = new Utils(targetWindow); 109 | this.eventHandler = this.utils.handler(); 110 | 111 | if ('onbeforeunload' in targetWindow) { 112 | unloadEvent = 'beforeunload'; 113 | } else if ('onpagehide' in targetWindow) { 114 | unloadEvent = 'pagehide'; 115 | } else { 116 | unloadEvent = 'unload'; 117 | } 118 | 119 | try { 120 | visibilityEvent = new CustomEvent(visibilityEventName); 121 | } catch (e) { 122 | visibilityEvent = targetWindow.document.createEvent('CustomEvent'); 123 | visibilityEvent.initCustomEvent(visibilityEventName, false, false, {}); 124 | } 125 | let requestAnimationFrame = targetWindow.requestAnimationFrame || targetWindow.mozRequestAnimationFrame || targetWindow.webkitRequestAnimationFrame || targetWindow.msRequestAnimationFrame; 126 | 127 | if (requestAnimationFrame) { 128 | targetWindow.requestAnimationFrame = requestAnimationFrame; 129 | } else { 130 | let lastTime = 0; 131 | targetWindow.requestAnimationFrame = function (callback) { 132 | let currTime = Date.now(); 133 | let timeToCall = Math.max(0, 16 - (currTime - lastTime)); 134 | let id = targetWindow.setTimeout(function () { 135 | callback(currTime + timeToCall); 136 | }, timeToCall); 137 | lastTime = currTime + timeToCall; 138 | 139 | return id; 140 | }; 141 | } 142 | 143 | let timerFrequency = null; 144 | (function visibilityWatcher() { 145 | targetWindow.requestAnimationFrame(visibilityWatcher); 146 | if (timerFrequency) { 147 | return false; 148 | } 149 | timerFrequency = setTimeout(function () { 150 | targetWindow.dispatchEvent(visibilityEvent); 151 | timerFrequency = null; 152 | }, 250); 153 | })(); 154 | 155 | options = obj.options !== void 0 ? obj.options : {}; 156 | this.utils.initSystem(system); 157 | } 158 | 159 | /** 160 | * set custom variable. 161 | * @param {string} k key of the variable. 162 | * @param {Object|string|number\Array|Boolean} o variable to be set. 163 | */ 164 | setCustomVars(k, o) { 165 | context[k] = o; 166 | } 167 | 168 | /** 169 | * set custom object in ingest.context.custom_object 170 | * @param {string} k key of the object. 171 | * @param {Object} o object to be set. 172 | */ 173 | setCustomObject(k, o) { 174 | context.custom_object[k] = o; 175 | } 176 | 177 | /** 178 | * set user id of external tools(e.g. rtoaster). 179 | * @param {string} k key of the user id. 180 | * @param {string} o user id to be set. 181 | */ 182 | setCustomId(k, o) { 183 | user.external_ids[k] = o; 184 | } 185 | 186 | /** 187 | * delete custome variable set by `setCustomVars`. 188 | * @param {string} k key of the variable. 189 | */ 190 | delCustomVars(k) { 191 | delete context[k]; 192 | } 193 | 194 | /** 195 | * delete custom object set by `setCustomObject`. 196 | * @param {string} k key of the custom object. 197 | */ 198 | delCustomObject(k) { 199 | delete context.custom_object[k]; 200 | } 201 | 202 | /** 203 | * delete custom user id set by `setCustomId`. 204 | * @param {string} k key of the object. 205 | */ 206 | delCustomId(k) { 207 | delete user.external_ids[k]; 208 | } 209 | 210 | /** 211 | * re-attach event listeners to DOMs. 212 | */ 213 | initEventListeners() { 214 | if ((options.trackClick && options.trackClick.enable) || (options.trackLink && options.trackLink.enable) || (options.trackDownload && options.trackDownload.enable)) { 215 | this.delegateClickEvents({ 216 | 'trackClick': options.trackClick, 217 | 'trackLink': options.trackLink, 218 | 'trackDownload': options.trackDownload 219 | }); 220 | } 221 | if (options.trackUnload && options.trackUnload.enable) { 222 | this.setEventToUnload(); 223 | } 224 | if (options.trackScroll && options.trackScroll.enable) { 225 | this.trackScroll(options.trackScroll.granularity, options.trackScroll.threshold); 226 | } 227 | if (options.trackInfinityScroll && options.trackInfinityScroll.enable) { 228 | this.trackInfinityScroll(options.trackInfinityScroll.step, options.trackInfinityScroll.threshold); 229 | } 230 | if (options.trackRead && options.trackRead.enable) { 231 | this.trackRead(options.trackRead.target, options.trackRead.granularity, options.trackRead.milestones); 232 | } 233 | if (options.trackViewability && options.trackViewability.enable) { 234 | this.trackViewability(options.trackViewability.targets); 235 | } 236 | if (options.trackMedia && options.trackMedia.enable) { 237 | this.trackMedia(options.trackMedia.selector, options.trackMedia.heartbeat); 238 | } 239 | if (options.trackForm && options.trackForm.enable) { 240 | this.trackForm(options.trackForm.target); 241 | } 242 | if (options.trackPerformance && options.trackPerformance.enable) { 243 | atlasDOMContentLoadedHandler = () => { 244 | performanceInfo = this.utils.getP(); 245 | context.navigation_timing = performanceInfo.performanceResult || {}; 246 | context.navigation_type = performanceInfo.navigationType || {}; 247 | }; 248 | targetWindow.addEventListener('DOMContentLoaded', atlasDOMContentLoadedHandler, false); 249 | } 250 | if (options.trackThroughMessage && options.trackThroughMessage.enable) { 251 | this.trackThroughMessage(); 252 | } 253 | } 254 | 255 | /** 256 | * initialize atlas tracking per page. 257 | * @param {Object} obj initialization config object. 258 | */ 259 | initPage(obj) { 260 | const paramUser = obj.user; 261 | const paramContext = obj.context; 262 | 263 | if(isInitialized){ 264 | pageLoadedAt = prevActionOccurredAt = Date.now(); 265 | }else{ 266 | isInitialized = true; 267 | } 268 | 269 | if (paramUser !== void 0) { 270 | user = paramUser; 271 | user.custom_object = paramUser.custom_object || {}; 272 | user.external_ids = paramUser.external_ids || {}; 273 | } 274 | if (paramContext !== void 0) { 275 | context = paramContext; 276 | context.root_id = this.utils.getUniqueId(); 277 | context.page_title = paramContext.page_title !== void 0 ? paramContext.page_title : defaults.page_title; 278 | context.url = paramContext.url !== void 0 ? paramContext.url : defaults.url; 279 | context.referrer = paramContext.referrer !== void 0 ? paramContext.referrer : defaults.referrer; 280 | context.product_family = paramContext.product_family || defaults.product_family; 281 | context.product = paramContext.product || defaults.product; 282 | context.page_num = paramContext.page_num || 1; 283 | context.visibility = targetWindow.document.visibilityState || 'unknown'; 284 | context.custom_object = paramContext.custom_object || {}; 285 | context.funnel = paramContext.funnel || {}; 286 | 287 | if(options.trackClick.logLastClick){ 288 | const ttl = options.trackClick.lastClickTtl || 5; 289 | let atlasLastClickJson = ''; 290 | let atlasLastClickObj = {}; 291 | 292 | try{ 293 | atlasLastClickJson = sessionStorage.getItem(lastClickStorageKey); 294 | atlasLastClickObj = JSON.parse(atlasLastClickJson) || {}; 295 | sessionStorage.removeItem(lastClickStorageKey); 296 | }catch(e){ 297 | // Nothing to do 298 | } 299 | 300 | if((Date.now() - atlasLastClickObj.ts) <= (ttl * 1000)){ 301 | context.last_click = atlasLastClickObj.attr; 302 | } 303 | } 304 | } 305 | 306 | if (options.trackNavigation && options.trackNavigation.enable) { 307 | context.navigation = this.utils.getNav() || {}; 308 | } 309 | if (options.trackPerformance && options.trackPerformance.enable) { 310 | performanceInfo = this.utils.getP(); 311 | context.navigation_timing = performanceInfo.performanceResult || {}; 312 | context.navigation_type = performanceInfo.navigationType || {}; 313 | } 314 | 315 | this.initEventListeners(); 316 | } 317 | 318 | /** 319 | * remove tracking options and handlers 320 | */ 321 | disableTracking() { 322 | if ((options.trackClick && options.trackClick.enable) || (options.trackLink && options.trackLink.enable) || (options.trackDownload && options.trackDownload.enable)) { 323 | this.eventHandler.remove(eventHandlerKeys['click']); 324 | } 325 | if (options.trackUnload && options.trackUnload.enable) { 326 | this.eventHandler.remove(eventHandlerKeys['unload']); 327 | } 328 | if (options.trackScroll && options.trackScroll.enable) { 329 | this.eventHandler.remove(eventHandlerKeys['scroll']); 330 | } 331 | if (options.trackInfinityScroll && options.trackInfinityScroll.enable) { 332 | this.eventHandler.remove(eventHandlerKeys['infinityScroll']); 333 | } 334 | if (options.trackRead && options.trackRead.enable) { 335 | this.eventHandler.remove(eventHandlerKeys['read']); 336 | } 337 | if (options.trackViewability && options.trackViewability.enable) { 338 | this.eventHandler.remove(eventHandlerKeys['viewability']); 339 | } 340 | if (options.trackMedia && options.trackMedia.enable) { 341 | const targetEvents = ['play', 'pause', 'end']; 342 | for (let i = 0; i < targetEvents.length; i++) { 343 | this.eventHandler.remove(eventHandlerKeys['media'][targetEvents[i]]); 344 | } 345 | } 346 | if (options.trackForm && options.trackForm.enable && options.trackForm.target !== null) { 347 | const targetEvents = ['focus', 'change']; 348 | for (let i = 0; i < targetEvents.length; i++) { 349 | this.eventHandler.remove(eventHandlerKeys['form'][targetEvents[i]]); 350 | } 351 | } 352 | if (options.trackPerformance && options.trackPerformance.enable) { 353 | targetWindow.removeEventListener('DOMContentLoaded', atlasDOMContentLoadedHandler); 354 | } 355 | 356 | options = {}; 357 | } 358 | 359 | /** 360 | * track page view. 361 | */ 362 | trackPage() { 363 | this.utils.transmit('view', 'page', user, context, supplement); 364 | } 365 | 366 | /** 367 | * Send any data at any timing. 368 | * @param {string} [action='action'] describes how user interact. 369 | * @param {string} [category='unknown'] what the action parameter's subject. 370 | * @param {string} [events=null] describes what happened by the user's action. 371 | * @param {Object} [obj={}] custom variables. 372 | */ 373 | trackAction(action = 'action', category = 'unknown', events = null, obj = {}) { 374 | const now = Date.now(); 375 | context.events = events || null; 376 | this.utils.transmit(action, category, user, context, { 377 | 'action': { 378 | 'location': obj.location || undefined, 379 | 'destination': obj.destination || undefined, 380 | 'dataset': obj.dataset || undefined, 381 | 'name': obj.action_name || undefined, 382 | 'elapsed_since_page_load': (now - pageLoadedAt) / 1000, 383 | 'elapsed_since_prev_action': (now - prevActionOccurredAt) / 1000, 384 | 'content_id': obj.content_id || undefined, 385 | 'content_name': obj.content_name || undefined, 386 | 'custom_vars': obj.custom_vars || {} 387 | } 388 | }); 389 | context.events = null; 390 | prevActionOccurredAt = now; 391 | } 392 | 393 | /** 394 | * @private 395 | */ 396 | delegateClickEvents(obj) { 397 | this.eventHandler.remove(eventHandlerKeys['click']); 398 | eventHandlerKeys['click'] = this.eventHandler.add(targetWindow.document.body, 'click', (ev) => { 399 | const trackClickConfig = obj.trackClick; 400 | const targetAttribute = trackClickConfig && trackClickConfig.targetAttribute ? trackClickConfig.targetAttribute : false; 401 | const targetElement = this.utils.qsM('a, button, [role="button"]', ev.target, targetAttribute); 402 | 403 | if(targetElement && targetElement.element){ 404 | 405 | let elm = targetElement.element; 406 | let ext = (elm.pathname || '').match(/.+\/.+?\.([a-z]+([?#;].*)?$)/); 407 | let attr = { 408 | 'destination': elm.href || undefined, 409 | 'dataset': elm.dataset || undefined, 410 | 'target': elm.target || undefined, 411 | 'media': elm.media || undefined, 412 | 'type': elm.type || undefined, 413 | 'tag': elm.tagName.toLowerCase(), 414 | 'id': elm.id || undefined, 415 | 'class': elm.className || undefined, 416 | 'location': targetElement.pathDom || undefined, 417 | 'elapsed_since_page_load': ((Date.now()) - pageLoadedAt) / 1000 418 | }; 419 | 420 | // Outbound 421 | if (obj.trackLink && obj.trackLink.enable && elm.hostname && targetWindow.location.hostname !== elm.hostname && obj.trackLink.internalDomains.indexOf(elm.hostname) < 0) { 422 | attr['name'] = this.utils.getAttr(obj.trackLink.targetAttribute, elm); 423 | attr['text'] = !obj.trackLink.disableText ? (elm.innerText || elm.value || '').substr(0,63) : undefined; 424 | this.utils.transmit('open', 'outbound_link', user, context, { 425 | 'link': attr 426 | }); 427 | } 428 | 429 | // Download 430 | if (obj.trackDownload && obj.trackDownload.enable && elm.hostname && ext && obj.trackDownload.fileExtensions.indexOf(ext[1]) >= 0) { 431 | attr['name'] = this.utils.getAttr(obj.trackDownload.targetAttribute, elm); 432 | attr['text'] = !obj.trackDownload.disableText ? (elm.innerText || elm.value || '').substr(0,63) : undefined; 433 | this.utils.transmit('download', 'file', user, context, { 434 | 'download': attr 435 | }); 436 | } 437 | 438 | // Click 439 | if (trackClickConfig && trackClickConfig.enable && targetElement) { 440 | 441 | if( 442 | !targetAttribute 443 | || trackClickConfig.logAllClicks 444 | || (targetAttribute && elm.attributes[targetAttribute]) 445 | ){ 446 | attr['name'] = targetAttribute ? this.utils.getAttr(trackClickConfig.targetAttribute, elm) : undefined; 447 | attr['text'] = !trackClickConfig.disableText ? (elm.innerText || elm.value || '').substr(0,63) : undefined; 448 | 449 | if(targetElement.pathTrackable.length > 0){ 450 | attr['location'] = targetElement.pathTrackable; 451 | } 452 | 453 | // Last Click 454 | if(trackClickConfig.logLastClick){ 455 | try{ 456 | sessionStorage.setItem(lastClickStorageKey, JSON.stringify({ 457 | ts: Date.now(), 458 | attr: attr 459 | })); 460 | }catch(e){ 461 | // Nothing to do 462 | } 463 | } 464 | 465 | // If useLastClickOnly is false 466 | if(!trackClickConfig.useLastClickOnly){ 467 | this.utils.transmit('click', targetElement.category, user, context, { 468 | 'action': attr 469 | }); 470 | } 471 | } 472 | 473 | } 474 | } 475 | }, false); 476 | } 477 | 478 | /** 479 | * @private 480 | */ 481 | setEventToUnload() { 482 | this.eventHandler.remove(eventHandlerKeys['unload']); 483 | eventHandlerKeys['unload'] = this.eventHandler.add(targetWindow, unloadEvent, () => { 484 | this.utils.transmit('unload', 'page', user, context, { 485 | 'action': { 486 | 'name': 'leave_from_page', 487 | 'elapsed_since_page_load': ((Date.now()) - pageLoadedAt) / 1000 488 | } 489 | }); 490 | }, false); 491 | } 492 | 493 | /** 494 | * @private 495 | */ 496 | trackScroll(granularity, threshold) { 497 | const each = granularity || 25; 498 | const steps = 100 / each; 499 | const limit = threshold * 1000 || 2 * 1000; 500 | let now = Date.now(); 501 | let prev = now; 502 | let r = {}; //result 503 | let cvr = 0; //currentViewRate 504 | let pvr = 0; //prevViewRate 505 | this.eventHandler.remove(eventHandlerKeys['scroll']); 506 | eventHandlerKeys['scroll'] = this.eventHandler.add(targetWindow, visibilityEventName, () => { 507 | r = this.utils.getV(null); 508 | if (r.detail.documentIsVisible !== 'hidden' && r.detail.documentIsVisible !== 'prerender') { 509 | now = Date.now(); 510 | cvr = Math.round(r.detail.documentScrollRate * steps) * each; 511 | if (cvr > pvr && cvr >= 0 && cvr <= 100) { 512 | setTimeout(() => { 513 | if (cvr > pvr) { 514 | this.utils.transmit('scroll', 'page', user, context, { 515 | 'scroll_depth': { 516 | 'page_height': r.detail.documentHeight, 517 | 'viewed_until': r.detail.documentScrollUntil, 518 | 'viewed_percent': cvr, 519 | 'elapsed_since_page_load': (now - pageLoadedAt) / 1000, 520 | 'elapsed_since_prev_action': (now - prev) / 1000 521 | } 522 | }); 523 | pvr = cvr; 524 | } 525 | prev = now; 526 | }, limit); 527 | } 528 | } 529 | }, false); 530 | } 531 | 532 | /** 533 | * @private 534 | */ 535 | trackInfinityScroll(step, threshold) { 536 | const limit = threshold * 1000 || 2 * 1000; 537 | let now = Date.now(); 538 | let prev = now; 539 | let r = {}; //result 540 | let cvp = 0; //currentViewRate 541 | let pvp = 0; //prevViewRate 542 | this.eventHandler.remove(eventHandlerKeys['infinityScroll']); 543 | eventHandlerKeys['infinityScroll'] = this.eventHandler.add(targetWindow, visibilityEventName, () => { 544 | r = this.utils.getV(null); 545 | if (r.detail.documentIsVisible !== 'hidden' && r.detail.documentIsVisible !== 'prerender') { 546 | now = Date.now(); 547 | cvp = r.detail.documentScrollUntil; 548 | if (cvp > pvp && cvp >= pvp && cvp >= step) { 549 | setTimeout(() => { 550 | if (cvp > pvp) { 551 | this.utils.transmit('infinity_scroll', 'page', user, context, { 552 | 'scroll_depth': { 553 | 'page_height': r.detail.documentHeight, 554 | 'viewed_until': cvp, 555 | 'viewed_percent': r.detail.documentScrollRate, 556 | 'elapsed_since_page_load': (now - pageLoadedAt) / 1000, 557 | 'elapsed_since_prev_action': (now - prev) / 1000 558 | } 559 | }); 560 | pvp = cvp + step; 561 | } 562 | prev = now; 563 | }, limit); 564 | } 565 | } 566 | }, false); 567 | } 568 | 569 | /** 570 | * @private 571 | */ 572 | trackRead(target, granularity, milestones = []) { 573 | if (!target) { 574 | return; 575 | } 576 | const each = granularity || 25; 577 | const steps = 100 / each; 578 | let now = Date.now(); 579 | let prev = now; 580 | let r = {}; //result 581 | let eiv = 0; //elapsedInVisible 582 | let cvr = 0; //currentViewRate 583 | let pvr = 0; //prevViewRate 584 | this.eventHandler.remove(eventHandlerKeys['read']); 585 | eventHandlerKeys['read'] = this.eventHandler.add(targetWindow, visibilityEventName, () => { 586 | r = this.utils.getV(target); 587 | if (r.detail.documentIsVisible !== 'hidden' && r.detail.documentIsVisible !== 'prerender' && r.status.isInView) { 588 | now = Date.now(); 589 | if (now - prev >= 1000) { 590 | prev = now; 591 | } 592 | eiv = eiv + (now - prev); 593 | prev = now; 594 | 595 | // Milestone based 596 | if(milestones.length > 0 && milestones[0] <= (eiv / 1000)){ 597 | this.utils.transmit('read', 'article', user, context, { 598 | 'read': { 599 | 'mode': 'time', 600 | 'milestone': milestones[0], 601 | 'page_height': r.detail.documentHeight, 602 | 'element_height': r.detail.targetHeight, 603 | 'viewed_from': r.detail.targetVisibleTop, 604 | 'viewed_until': r.detail.targetVisibleBottom, 605 | 'viewed_percent': cvr, 606 | 'elapsed_since_page_load': (now - pageLoadedAt) / 1000, 607 | 'elapsed_since_prev_action': (now - prev) / 1000 608 | } 609 | }); 610 | milestones.shift(); 611 | } 612 | 613 | // Scroll based 614 | cvr = Math.round(r.detail.targetScrollRate * steps) * each; 615 | if (cvr > pvr && cvr >= 0 && cvr <= 100) { 616 | setTimeout(() => { 617 | if (cvr > pvr) { 618 | this.utils.transmit('read', 'article', user, context, { 619 | 'read': { 620 | 'mode': 'scroll', 621 | 'page_height': r.detail.documentHeight, 622 | 'element_height': r.detail.targetHeight, 623 | 'viewed_from': r.detail.targetVisibleTop, 624 | 'viewed_until': r.detail.targetVisibleBottom, 625 | 'viewed_percent': cvr, 626 | 'elapsed_since_page_load': (now - pageLoadedAt) / 1000, 627 | 'elapsed_since_prev_action': (now - prev) / 1000 628 | } 629 | }); 630 | pvr = cvr; 631 | } 632 | }, 1000); 633 | } 634 | } 635 | }, false); 636 | } 637 | 638 | /** 639 | * @private 640 | */ 641 | trackViewability(targets) { 642 | let now = Date.now(); 643 | let r = {}; //results 644 | let f = {}; //flags 645 | this.eventHandler.remove(eventHandlerKeys['viewability']); 646 | eventHandlerKeys['viewability'] = this.eventHandler.add(targetWindow, visibilityEventName, () => { 647 | for (let i = 0; i < targets.length; i++) { 648 | if (targets[i]) { 649 | r[i] = this.utils.getV(targets[i]); 650 | if (r[i].status.isInView === true && r[i].detail.documentIsVisible !== 'hidden' && r[i].detail.documentIsVisible !== 'prerender' && r[i].detail.targetViewableRate >= 0.5 && !f[i]) { 651 | setTimeout(() => { 652 | if (r[i].detail.targetViewableRate >= 0.5 && !f[i]) { 653 | now = Date.now(); 654 | f[i] = true; 655 | this.utils.transmit('viewable_impression', 'ad', user, context, { 656 | 'viewability': { 657 | 'page_height': r[i].detail.documentHeight, 658 | 'element_order_in_target': i, 659 | 'element_tag': targets[i].tagName, 660 | 'element_class': targets[i].className, 661 | 'element_id': targets[i].id, 662 | 'element_height': r[i].detail.targetHeight, 663 | 'elapsed_since_page_load': (now - pageLoadedAt) / 1000 664 | } 665 | }); 666 | } 667 | }, 1000); 668 | } 669 | } 670 | } 671 | }, false); 672 | } 673 | 674 | /** 675 | * @private 676 | */ 677 | trackMedia(selector, heartbeat) { 678 | const targetEvents = ['play', 'pause', 'end']; 679 | let f = {}; //flags 680 | for (let i = 0; i < targetEvents.length; i++) { 681 | this.eventHandler.remove(eventHandlerKeys['media'][targetEvents[i]]); 682 | eventHandlerKeys['media'][targetEvents[i]] = this.eventHandler.add(targetWindow.document.body, targetEvents[i], (ev) => { 683 | if (this.utils.qsM(selector, ev.target)) { 684 | const details = this.utils.getM(ev.target); 685 | this.utils.transmit(targetEvents[i], details.tag, user, context, { 686 | 'media': details 687 | }); 688 | } 689 | }, {capture: true}); 690 | } 691 | 692 | this.eventHandler.remove(eventHandlerKeys['media']['timeupdate']); 693 | eventHandlerKeys['media']['timeupdate'] = this.eventHandler.add(targetWindow.document, 'timeupdate', (ev) => { 694 | if (this.utils.qsM(selector, ev.target)) { 695 | const details = this.utils.getM(ev.target); 696 | const index = details.tag + '-' + details.id + '-' + details.src; 697 | if (f[index]) { 698 | return false; 699 | } 700 | f[index] = setTimeout(() => { 701 | if (ev.target.paused !== true && ev.target.ended !== true) { 702 | this.utils.transmit('playing', details.tag, user, context, { 703 | 'media': details 704 | }); 705 | } 706 | f[index] = false; 707 | }, heartbeat * 1000); 708 | } 709 | }, {capture: true}); 710 | } 711 | 712 | trackForm(target) { 713 | if (!target) { 714 | return; 715 | } 716 | const targetEvents = ['focus', 'change']; 717 | let f = { 718 | 'name': target.name || target.id || '-', 719 | 'dataset': target.dataset, 720 | 'items_detail': {} 721 | }; 722 | for (let i = 0; i < targetEvents.length; i++) { 723 | this.eventHandler.remove(eventHandlerKeys['form'][targetEvents[i]]); 724 | eventHandlerKeys['form'][targetEvents[i]] = this.eventHandler.add(target, targetEvents[i], (ev) => { 725 | f = this.utils.getF(f, targetEvents[i], ev.target, pageLoadedAt); 726 | }, false); 727 | } 728 | this.eventHandler.remove(eventHandlerKeys['unload']); 729 | eventHandlerKeys['unload'] = this.eventHandler.add(targetWindow, unloadEvent, () => { 730 | this.utils.transmit('track', 'form', user, context, { 731 | 'form': f 732 | }); 733 | }, false); 734 | } 735 | 736 | trackThroughMessage() { 737 | this.eventHandler.remove(eventHandlerKeys['message']); 738 | eventHandlerKeys['message'] = this.eventHandler.add(targetWindow, 'message', (msg) => { 739 | let attributes = {}; 740 | try{ 741 | attributes = JSON.parse(msg.data.attributes); 742 | }catch(e){ 743 | // Nothing to do... 744 | } 745 | if(msg && msg.data && msg.data.isAtlasEvent){ 746 | this.utils.transmit( 747 | msg.data.action, 748 | msg.data.category, 749 | user, 750 | context, 751 | attributes 752 | ); 753 | } 754 | }, false); 755 | } 756 | } 757 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // SDK Version Info 4 | const SDK_VERSION = process.env.npm_package_version; 5 | const SDK_API_KEY = process.env.SDK_API_KEY || 'test_api_key'; 6 | const DEFAULT_ENDPOINT = process.env.DEFAULT_ENDPOINT || 'atlas.local'; 7 | 8 | // Nginx URL Length default Limit 9 | // See: http://nginx.org/en/docs/http/ngx_http_core_module.html#large_client_header_buffers 10 | const MAX_REQUEST_URL_LENGTH = 8 * (1 << 10); 11 | 12 | const HTTP_METHOD_GET = 'GET'; 13 | const HTTP_METHOD_POST = 'POST'; 14 | const HTTP_HEADER_CONTENT_TYPE = 'Content-Type'; 15 | const HTTP_HEADER_CONTENT_TYPE_APPLICATION_JSON = 'application/json'; 16 | 17 | const sdkName = 'ATJ'; 18 | const atlasCookieName = 'atlasId'; 19 | 20 | let atlasEndpoint = null; 21 | let atlasApiKey = null; 22 | let atlasBeaconTimeout = null; 23 | let atlasId = '0'; 24 | let handlerEvents = {}; 25 | let handlerKey = 0; 26 | let sendBeaconStatus = true; 27 | 28 | /** 29 | * @ignore 30 | */ 31 | export default class Utils { 32 | constructor(targetWindow) { 33 | const timestamp = Math.floor(Date.now() / 1000).toString(16); 34 | const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; 35 | let result = ''; 36 | if (self.crypto && self.crypto.getRandomValues) { 37 | const u32a = new Uint8Array(32); 38 | self.crypto.getRandomValues(u32a); 39 | result = Array.from( 40 | u32a, 41 | (byte) => chars[byte % chars.length], 42 | ).join(""); 43 | }else{ 44 | // For IE compatibility 45 | for (let i = 0; i < 32; i++) { 46 | result += chars[Math.floor(Math.random() * chars.length)]; 47 | } 48 | } 49 | this.uniqueId = `${timestamp}.${(result.substring(0,32))}`; 50 | this.targetWindow = targetWindow; 51 | } 52 | 53 | initSystem(system) { 54 | atlasEndpoint = system.endpoint ? system.endpoint : DEFAULT_ENDPOINT; 55 | atlasApiKey = system.apiKey ? system.apiKey : SDK_API_KEY; 56 | atlasBeaconTimeout = system.beaconTimeout ? system.beaconTimeout : 2000; 57 | 58 | atlasId = this.getC(atlasCookieName); 59 | 60 | if (!atlasId || atlasId === '0' || atlasId === 0 || atlasId === '1' || atlasId === 1 || atlasId.length < 5) { 61 | atlasId = this.uniqueId; 62 | } 63 | } 64 | 65 | qsM(s, t, d = false) { 66 | let e = null; // Trackable Element 67 | let c = 'button'; 68 | let pt = []; // Path of data-trackable 69 | let pd = []; // Path of elements 70 | if (t.nodeType === 3) { 71 | t = t.parentNode; 72 | } 73 | while (t && t !== this.targetWindow.document) { 74 | let matches = ( 75 | t.matches || 76 | t.msMatchesSelector || 77 | function () { 78 | return false; 79 | } 80 | ).bind(t); 81 | 82 | let elm = t.tagName.toLowerCase(); 83 | if (elm !== 'html' && elm !== 'body') { 84 | if (t.id) { 85 | elm += `#${t.id}`; 86 | } 87 | if (t.className) { 88 | elm += `.${t.className}`; 89 | } 90 | pd.unshift(elm); 91 | } 92 | 93 | if (d) { 94 | if (t.hasAttribute(d)) { 95 | pt.unshift(t.getAttribute(d)); 96 | } 97 | } 98 | 99 | if (!e && matches(s)) { 100 | if (t.tagName.toLowerCase() === 'a') { 101 | c = 'link'; 102 | } else { 103 | c = t.tagName.toLowerCase(); 104 | } 105 | e = t; 106 | } 107 | 108 | t = t.parentNode; 109 | } 110 | 111 | return { 112 | 'element': e, 113 | 'category': c, 114 | 'pathTrackable': pt.join('>'), 115 | 'pathDom': pd.join('>') 116 | }; 117 | 118 | } 119 | 120 | getAttr(attributeName, element) { 121 | let result = null; 122 | if(attributeName){ 123 | result = element.getAttribute(attributeName); 124 | } 125 | if(result === null) { 126 | result = undefined; 127 | } 128 | 129 | return result; 130 | } 131 | 132 | getC(k) { 133 | const cookies = this.targetWindow.document.cookie.split(';'); 134 | for(let i = 0; i < cookies.length; i++) { 135 | let cookie = cookies[i]; 136 | while (cookie.charAt(0) === ' ') { 137 | cookie = cookie.substring(1, cookie.length); 138 | } 139 | if (cookie.indexOf(`${k}=`) === 0) { 140 | return cookie.substring(`${k}=`.length, cookie.length); 141 | } 142 | } 143 | 144 | return ''; 145 | } 146 | 147 | getQ(k) { 148 | const s = this.targetWindow.location.search.slice(1); 149 | if (s === '') { 150 | return ''; 151 | } 152 | const q = s.split('&'); 153 | const l = q.length; 154 | for (let i = 0; i < l; ++i) { 155 | const pair = q[i].split('='); 156 | if (decodeURIComponent(pair[0]) === k) { 157 | return decodeURIComponent(pair[1]); 158 | } 159 | } 160 | 161 | return ''; 162 | } 163 | 164 | getLS(k) { 165 | let r = ''; 166 | try { 167 | r = this.targetWindow.localStorage[k]; 168 | } catch (e) { 169 | // Nothing to do... 170 | } 171 | 172 | return r; 173 | } 174 | 175 | getNav() { 176 | let nav = { 177 | history_length: this.targetWindow.history.length 178 | }; 179 | if ('performance' in this.targetWindow) { 180 | let p = this.targetWindow.performance; 181 | if ('getEntriesByType' in p) { 182 | let navs = p.getEntriesByType('navigation'); 183 | if(navs.length >= 1) { 184 | nav.type = navs[0].type; 185 | nav.redirectCount = navs[0].redirectCount; 186 | nav.domContentLoaded = navs[0].domContentLoadedEventStart; 187 | } 188 | } 189 | if ('getEntriesByName' in p) { 190 | let paints = p.getEntriesByName('first-paint'); 191 | if(paints.length >= 1) { 192 | nav.first_paint = paints[0].startTime; 193 | } 194 | } 195 | } 196 | 197 | return nav; 198 | } 199 | 200 | getP() { 201 | const tsDiff = function(tsStart, tsEnd){ 202 | return (tsEnd - tsStart < 0 || tsEnd - tsStart > 3600000) ? undefined : Math.round(tsEnd - tsStart); 203 | }; 204 | let p = {}; // Performance Timing 205 | let t = {}; // Navigation Type 206 | let r = {}; // Result 207 | if ('performance' in this.targetWindow) { 208 | p = this.targetWindow.performance.timing; 209 | r = { 210 | 'unload': tsDiff(p.unloadEventStart, p.unloadEventEnd), 211 | 'redirect': tsDiff(p.redirectStart, p.redirectEnd), 212 | 'dns': tsDiff(p.domainLookupStart, p.domainLookupEnd), 213 | 'tcp': tsDiff(p.connectStart, p.connectEnd), 214 | 'request': tsDiff(p.requestStart, p.responseStart), 215 | 'response': tsDiff(p.responseStart, p.responseEnd), 216 | 'dom': tsDiff(p.domLoading, p.domContentLoadedEventStart), 217 | 'domContent': tsDiff(p.domContentLoadedEventStart, p.domContentLoadedEventEnd), 218 | 'onload': tsDiff(p.loadEventStart, p.loadEventEnd), 219 | 'untilResponseComplete': tsDiff(p.navigationStart, p.responseEnd), 220 | 'untilDomComplete': tsDiff(p.navigationStart, p.domContentLoadedEventStart) 221 | }; 222 | t = (this.targetWindow.performance || {}).navigation; 223 | } 224 | 225 | return { 226 | 'performanceResult': r, 227 | 'navigationType': t 228 | }; 229 | } 230 | 231 | handler() { 232 | return { 233 | add: function (target, type, listener, capture) { 234 | target.addEventListener(type, listener, capture); 235 | handlerEvents[handlerKey] = { 236 | target: target, 237 | type: type, 238 | listener: listener, 239 | capture: capture 240 | }; 241 | 242 | return handlerKey++; 243 | }, 244 | remove: function (handlerKey) { 245 | if (handlerKey in handlerEvents) { 246 | let e = handlerEvents[handlerKey]; 247 | e.target.removeEventListener(e.type, e.listener, e.capture); 248 | } 249 | } 250 | }; 251 | } 252 | 253 | getV(t) { 254 | let tgr = {}; //targetRect 255 | try { 256 | tgr = t.getBoundingClientRect(); 257 | } catch (e) { 258 | // Nothing to do... 259 | } 260 | 261 | const vph = this.targetWindow.innerHeight; //viewportHeight 262 | const dch = this.targetWindow.document.documentElement.scrollHeight; //documentHeight 263 | const div = this.targetWindow.document.visibilityState || 'unknown'; //documentIsVisible 264 | const dvt = 'pageYOffset' in this.targetWindow ? 265 | this.targetWindow.pageYOffset : 266 | (this.targetWindow.document.documentElement || this.targetWindow.document.body.parentNode || this.targetWindow.document.body).scrollTop; //documentVisibleTop 267 | const dvb = dvt + vph; //documentVisibleBottom 268 | const tgh = tgr.height; //targetHeight 269 | const tmt = tgr.top <= 0 ? 0 : tgr.top; //targetMarginTop 270 | const tmb = (tgr.bottom - vph) * -1 <= 0 ? 0 : (tgr.bottom - vph) * -1; //targetMarginBottom 271 | const dsu = dvb; //documentScrollUntil 272 | const dsr = dvb / dch; //documentScrollRate 273 | 274 | let tvt = null; //targetVisibleTop 275 | let tvb = null; //targetVisibleBottom 276 | let tsu = 0; //targetScrollUntil 277 | let tsr = 0; //targetScrollRate 278 | let tvr = 0; //targetViewableRate 279 | let iiv = false; //isInView 280 | let loc = null; //location 281 | 282 | if (tgr.top >= 0 && tgr.bottom > vph && tgr.top >= vph) { 283 | // pre 284 | tvt = null; 285 | tvb = null; 286 | iiv = false; 287 | loc = 'pre'; 288 | } else if (tgr.top >= 0 && tgr.bottom > vph && tgr.top < vph) { 289 | // top 290 | tvt = 0; 291 | tvb = vph - tgr.top; 292 | iiv = true; 293 | loc = 'top'; 294 | } else if (tgr.top < 0 && tgr.bottom > vph) { 295 | // middle 296 | tvt = tgr.top * -1; 297 | tvb = tvt + vph; 298 | iiv = true; 299 | loc = 'middle'; 300 | } else if (tgr.top >= 0 && tgr.bottom <= vph) { 301 | // all in 302 | tvt = 0; 303 | tvb = tgh; 304 | iiv = true; 305 | loc = 'all'; 306 | } else if (tgr.top < 0 && tgr.bottom >= 0 && tgr.bottom <= vph) { 307 | // bottom 308 | tvt = tgh + tgr.top; 309 | tvb = tgh; 310 | iiv = true; 311 | loc = 'bottom'; 312 | } else if (tgr.top < 0 && tgr.bottom < 0) { 313 | // post 314 | tvt = null; 315 | tvb = null; 316 | iiv = false; 317 | loc = 'post'; 318 | } else { 319 | iiv = false; 320 | loc = 'unknown'; 321 | } 322 | 323 | tvr = (tvb - tvt) / tgh; 324 | tsu = tvb; 325 | tsr = tsu / tgh; 326 | 327 | return { 328 | 'detail': { 329 | 'viewportHeight': vph, 330 | 'documentHeight': dch, 331 | 'documentIsVisible': div, 332 | 'documentVisibleTop': dvt, 333 | 'documentVisibleBottom': dvb, 334 | 'targetHeight': tgh, 335 | 'targetVisibleTop': tvt, 336 | 'targetVisibleBottom': tvb, 337 | 'targetMarginTop': tmt, 338 | 'targetMarginBottom': tmb, 339 | 'targetScrollUntil': tsu, 340 | 'targetScrollRate': tsr, 341 | 'targetViewableRate': tvr, 342 | 'documentScrollUntil': dsu, 343 | 'documentScrollRate': dsr 344 | }, 345 | 'status': { 346 | 'isInView': iiv, 347 | 'location': loc 348 | } 349 | }; 350 | } 351 | 352 | getM(t) { 353 | if (t !== void 0) { 354 | return { 355 | 'tag': t.nodeName.toLowerCase() || 'na', 356 | 'id': t.id || 'na', 357 | 'src': t.src || 'na', 358 | 'type': t.type || undefined, 359 | 'codecs': t.codecs || undefined, 360 | 'muted': t.muted || false, 361 | 'default_muted': t.defaultMuted || false, 362 | 'autoplay': t.autoplay || false, 363 | 'width': t.clientWidth || undefined, 364 | 'height': t.clientHeight || undefined, 365 | 'player_id': t.playerId || undefined, 366 | 'played_percent': Math.round(t.currentTime / t.duration * 100), 367 | 'duration': t.duration, 368 | 'current_time': Math.round(t.currentTime * 10) / 10, 369 | 'dataset': t.dataset 370 | }; 371 | } 372 | } 373 | 374 | getF(f, e, t, pl) { 375 | const n = t.name || t.id || '-'; 376 | let l = 0; 377 | if (t.tagName.toLowerCase() === 'select') { 378 | let a = []; 379 | for (let i = 0; i < t.length; i++) { 380 | if (t[i].selected) { 381 | a.push(true); 382 | } 383 | } 384 | l = a.length; 385 | } else if (t.tagName.toLowerCase() === 'input' && (t.type === 'checkbox' || t.type === 'radio')) { 386 | if (t.checked) { 387 | l = 1; 388 | } else { 389 | l = 0; 390 | } 391 | } else { 392 | l = t.value.length; 393 | } 394 | if (t.type !== 'hidden') { 395 | f.items_detail[n] = { 396 | 'status': e, 397 | 'length': l 398 | }; 399 | } 400 | if (!f.first_item) { 401 | f.first_item = t.name || t.id || '-'; 402 | f.first_item_since_page_load = (Date.now() - pl) / 1000; 403 | } 404 | f.last_item = t.name || t.id || '-'; 405 | f.last_item_since_page_load = (Date.now() - pl) / 1000; 406 | f.last_item_since_first_item = f.last_item_since_page_load - f.first_item_since_page_load; 407 | 408 | return f; 409 | } 410 | 411 | getUniqueId() { 412 | return this.uniqueId; 413 | } 414 | 415 | buildIngest(u, c, s) { 416 | let igt = { 417 | 'user': u, 418 | 'context': {} 419 | }; //ingest 420 | let lyt = 'unknown'; //layout 421 | if (this.targetWindow.orientation) { 422 | lyt = ((Math.abs(this.targetWindow.orientation) === 90) ? 'landscape' : 'portrait'); 423 | } 424 | 425 | for (let i in c) { 426 | igt.context[i] = c[i]; 427 | } 428 | for (let i in s) { 429 | igt.context[i] = s[i]; 430 | } 431 | const currentTime = new Date(); 432 | igt.user.timezone = currentTime.getTimezoneOffset() / 60 * -1; 433 | igt.user.timestamp = currentTime.toISOString(); 434 | igt.user.viewport_height = this.targetWindow.innerHeight; 435 | igt.user.viewport_width = this.targetWindow.innerWidth; 436 | igt.user.screen_height = this.targetWindow.screen.height; 437 | igt.user.screen_width = this.targetWindow.screen.width; 438 | igt.user.layout = lyt; 439 | 440 | return igt; 441 | } 442 | 443 | compress(v) { 444 | let r = v; 445 | r = r.replace(/%22%7D%2C%22/g, '%z'); 446 | r = r.replace(/%22%3A%22/g, '%y'); 447 | r = r.replace(/%22%2C%22/g, '%x'); 448 | r = r.replace(/%22%3A%7B/g, '%w'); 449 | r = r.replace(/%22%3A/g, '%v'); 450 | r = r.replace(/%2C%22/g, '%u'); 451 | r = r.replace(/%7D%7D%7D/g, '%t'); 452 | 453 | return r; 454 | } 455 | 456 | xhr(u, a, m, b) { 457 | const x = new XMLHttpRequest(); 458 | x.open(m, u, a); 459 | if (a === true) { 460 | x.timeout = atlasBeaconTimeout; 461 | } 462 | x.withCredentials = true; 463 | 464 | try{ 465 | if (m === HTTP_METHOD_POST && b) { 466 | x.setRequestHeader(HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_CONTENT_TYPE_APPLICATION_JSON); 467 | x.send(new Blob([b], { type: HTTP_HEADER_CONTENT_TYPE_APPLICATION_JSON })); 468 | } else { 469 | x.send(); 470 | } 471 | }catch(e){ 472 | // Nothing to do... 473 | } 474 | } 475 | 476 | transmit(ac, ca, ur, ct, sp) { 477 | const now = Date.now(); 478 | const a = (!(ac === 'unload' && ca === 'page')); //async 479 | let f = 1; //fpcStatus 480 | if (this.getC(atlasCookieName) !== atlasId) { 481 | f = 0; 482 | } 483 | 484 | const b = JSON.stringify(this.buildIngest(ur, ct, sp)); 485 | const _u = `https://${atlasEndpoint}/${sdkName}-${SDK_VERSION}/${now}/${encodeURIComponent(atlasId)}/${f}` 486 | + `/ingest?k=${atlasApiKey}&a=${ac}&c=${ca}&aqe=%`; //endpointUrl 487 | 488 | let u = _u + `&d=${this.compress(encodeURIComponent(b))}`; 489 | let m = HTTP_METHOD_GET; 490 | 491 | // Calculates the number of bytes in the URL string length 492 | // and switches to POST if it is greater than MAX_REQUEST_URL_LENGTH 493 | if (u.replace(/%../g, '*').length > MAX_REQUEST_URL_LENGTH) { 494 | m = HTTP_METHOD_POST; 495 | u = _u; 496 | } 497 | 498 | if ('sendBeacon' in navigator && sendBeaconStatus === true) { 499 | try { 500 | let blob = null; 501 | if (m == HTTP_METHOD_POST && b) { 502 | blob = new Blob([b], { type: HTTP_HEADER_CONTENT_TYPE_APPLICATION_JSON }); 503 | } 504 | 505 | sendBeaconStatus = navigator.sendBeacon(u, blob); 506 | } catch (e) { 507 | sendBeaconStatus = false; 508 | } 509 | if (!sendBeaconStatus) { 510 | this.xhr(u, a, m, b); 511 | } 512 | 513 | return true; 514 | } else { 515 | const targetWindow = this.targetWindow; 516 | if (('fetch' in targetWindow && typeof targetWindow.fetch === 'function') 517 | && ('AbortController' in targetWindow && typeof targetWindow.AbortController === 'function')) { 518 | const controller = new targetWindow.AbortController(); 519 | const signal = controller.signal; 520 | setTimeout(() => controller.abort(), atlasBeaconTimeout); 521 | try{ 522 | const options = { signal, method: m, cache: 'no-store', keepalive: true }; 523 | if (m === HTTP_METHOD_POST && b) { 524 | options['body'] = new Blob([b], { type: HTTP_HEADER_CONTENT_TYPE_APPLICATION_JSON }); 525 | options['headers'] = {}; 526 | options['headers'][HTTP_HEADER_CONTENT_TYPE] = HTTP_HEADER_CONTENT_TYPE_APPLICATION_JSON; 527 | } 528 | 529 | this.targetWindow.fetch(u, options); 530 | }catch(e){ 531 | // Nothing to do... 532 | } 533 | } else { 534 | this.xhr(u, a, m, b); 535 | } 536 | 537 | return true; 538 | } 539 | } 540 | 541 | setSendBeaconStatusForTestUse(st) { 542 | sendBeaconStatus = !!st; 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import "./utils.test.js"; 2 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | test 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | import Utils from '../src/utils'; 2 | import {expect} from 'chai'; 3 | import sinon from 'sinon'; 4 | 5 | describe('#getP', () => { 6 | let utils; 7 | beforeEach(() => { 8 | utils = new Utils(window.parent) 9 | }) 10 | 11 | it('should return performance info.', () => { 12 | let info = utils.getP() 13 | let t = info.navigationType 14 | let r = info.performanceResult 15 | 16 | expect(t).to.be.a('PerformanceNavigation') 17 | expect(r.unload).to.not.be.NaN 18 | expect(r.redirect).to.not.be.NaN 19 | expect(r.dns).to.not.be.NaN 20 | expect(r.request).to.not.be.NaN 21 | expect(r.response).to.not.be.NaN 22 | expect(r.dom).to.not.be.NaN 23 | expect(r.domContent).to.not.be.NaN 24 | expect(r.onload).to.not.be.NaN 25 | expect(r.untilResponseComplete).to.not.be.NaN 26 | expect(r.untilDomComplete).to.not.be.NaN 27 | }) 28 | }) 29 | 30 | describe('#transmit', () => { 31 | const utils = new Utils(window.parent); 32 | const sendBeaconStub = sinon.stub(navigator, 'sendBeacon'); 33 | const xhrStub = sinon.stub(utils, 'xhr'); 34 | const fetchStub = sinon.stub(utils.targetWindow, 'fetch'); 35 | 36 | const byteSize = (s) => { 37 | return s.replace(/%../g, '*').length 38 | }; 39 | 40 | before(() => { 41 | utils.initSystem({ 42 | endpoint: "example.com", 43 | apiKey: "xxxxxxxxx", 44 | }) 45 | }) 46 | 47 | beforeEach(() => { 48 | sendBeaconStub.reset(); 49 | xhrStub.reset(); 50 | fetchStub.reset(); 51 | 52 | utils.setSendBeaconStatusForTestUse(true); 53 | }) 54 | 55 | it('should send ingest data by sendBeacon with HTTP POST with Body when request url size is greater than 8k', () => { 56 | // setup stub 57 | sendBeaconStub.returns(true); 58 | 59 | // call HTTP request 60 | utils.transmit('unload', 'page', { 61 | "custom": "x".repeat(8 * (1 << 10)) 62 | }, {}, {}); 63 | 64 | // asserts 65 | expect(sendBeaconStub.callCount).to.be.equal(1); 66 | 67 | const sendBeaconArgs = sendBeaconStub.getCall(0).args; 68 | expect(sendBeaconArgs[1]).to.not.null; 69 | expect(sendBeaconArgs[1].size).to.be.gt(8 * 1 << 10) 70 | expect(sendBeaconArgs[1].type).to.be.equal("application/json") 71 | expect(xhrStub.callCount).to.be.equal(0); 72 | }) 73 | 74 | it('should send ingest data by xhr with HTTP POST with Body when request url size is greater than 8k', () => { 75 | // setup stub 76 | sendBeaconStub.returns(false); 77 | 78 | // call HTTP request 79 | utils.transmit('unload', 'page', { 80 | "custom": "x".repeat(8 * (1 << 10)) 81 | }, {}, {}); 82 | 83 | // asserts 84 | expect(xhrStub.callCount).to.be.equal(1); 85 | 86 | const xhrStubArgs = xhrStub.getCall(0).args 87 | expect(xhrStubArgs[2]).to.be.equal('POST') 88 | expect(byteSize(xhrStubArgs[3])).to.be.gt(8 * 1 << 10); 89 | }) 90 | 91 | it('should send ingest data by fetch with HTTP POST with Body when request url size is greater than 8k', () => { 92 | // setup 93 | utils.setSendBeaconStatusForTestUse(false); 94 | 95 | // call HTTP request 96 | utils.transmit('unload', 'page', { 97 | "custom": "x".repeat(8 * (1 << 10)) 98 | }, {}, {}); 99 | 100 | // asserts 101 | expect(fetchStub.callCount).to.be.equal(1); 102 | const fetchStubArgs = fetchStub.getCall(0).args 103 | 104 | expect(fetchStubArgs[1]['method']).to.be.equal('POST') 105 | expect(fetchStubArgs[1]['body'].size).to.be.gt(8 * 1 << 10); 106 | expect(fetchStubArgs[1]['body'].type).to.be.equal('application/json'); 107 | expect(fetchStubArgs[1]['headers']['Content-Type']).to.be.equal('application/json'); 108 | expect(sendBeaconStub.callCount).to.be.equal(0); 109 | expect(xhrStub.callCount).to.be.equal(0); 110 | }) 111 | 112 | it('should send ingest data by xhr with HTTP POST with Body when request url size is greater than 8k and fetch function is not implemented.', () => { 113 | // setup stub 114 | utils.setSendBeaconStatusForTestUse(false); 115 | 116 | const originalFetch = utils.targetWindow.fetch; 117 | delete utils.targetWindow.fetch; 118 | 119 | // call HTTP request 120 | utils.transmit('unload', 'page', { 121 | "custom": "x".repeat(8 * (1 << 10)) 122 | }, {}, {}); 123 | 124 | // asserts 125 | expect(fetchStub.callCount).to.be.equal(0); 126 | expect(sendBeaconStub.callCount).to.be.equal(0); 127 | 128 | const xhrStubArgs = xhrStub.getCall(0).args 129 | expect(xhrStubArgs[2]).to.be.equal('POST') 130 | expect(byteSize(xhrStubArgs[3])).to.be.gt(8 * 1 << 10); 131 | 132 | // cleanup 133 | utils.targetWindow.fetch = originalFetch; 134 | }) 135 | 136 | 137 | it('should send ingest data by sendBeacon with HTTP GET when request url size is less than 8k', () => { 138 | // setup stub 139 | sendBeaconStub.returns(true); 140 | 141 | // call HTTP request 142 | utils.transmit('unload', 'page', { custom_object: {} }, {}, {}); 143 | 144 | // asserts 145 | expect(sendBeaconStub.callCount).to.be.equal(1); 146 | expect(sendBeaconStub.getCall(0).args[1]).to.be.null; 147 | expect(byteSize(sendBeaconStub.getCall(0).args[0])).to.be.lt(8 * 1 << 10); 148 | expect(xhrStub.callCount).to.be.equal(0); 149 | }) 150 | 151 | it('should send ingest data by fetch with HTTP GET when request url size is less than 8k', () => { 152 | // setup 153 | utils.setSendBeaconStatusForTestUse(false); 154 | 155 | // call HTTP request 156 | utils.transmit('unload', 'page', { custom_object: {} }, {}, {}); 157 | 158 | // asserts 159 | expect(fetchStub.callCount).to.be.equal(1); 160 | expect(fetchStub.getCall(0).args[1]['method']).to.be.equal('GET') 161 | expect(byteSize(fetchStub.getCall(0).args[0])).to.be.lt(8 * 1 << 10); 162 | expect(sendBeaconStub.callCount).to.be.equal(0); 163 | expect(xhrStub.callCount).to.be.equal(0); 164 | }) 165 | 166 | it('should send ingest data by xhr with HTTP GET when request url size is less than 8k and fetch function is not implemented.', () => { 167 | // setup stub 168 | utils.setSendBeaconStatusForTestUse(false); 169 | sendBeaconStub.returns(true); 170 | 171 | const originalFetch = utils.targetWindow.fetch; 172 | delete utils.targetWindow.fetch; 173 | 174 | // call HTTP request 175 | utils.transmit('unload', 'page', { custom_object: {} }, {}, {}); 176 | 177 | // asserts 178 | expect(fetchStub.callCount).to.be.equal(0); 179 | expect(sendBeaconStub.callCount).to.be.equal(0); 180 | expect(xhrStub.callCount).to.be.equal(1); 181 | expect(byteSize(xhrStub.getCall(0).args[0])).to.be.lt(8 * 1 << 10); 182 | expect(xhrStub.getCall(0).args[2]).to.be.equal('GET') 183 | 184 | // cleanup 185 | utils.targetWindow.fetch = originalFetch; 186 | }) 187 | 188 | it('should send ingest data by xhr with HTTP GET when request url size is less than 8k', () => { 189 | // setup stub 190 | sendBeaconStub.returns(false); 191 | 192 | // call HTTP request 193 | utils.transmit('unload', 'page', { custom_object: {} }, {}, {}); 194 | 195 | // asserts 196 | expect(xhrStub.callCount).to.be.equal(1); 197 | expect(xhrStub.getCall(0).args[2]).to.be.equal('GET') 198 | expect(byteSize(xhrStub.getCall(0).args[0])).to.be.lt(8 * 1 << 10); 199 | }) 200 | }) 201 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | module: { 5 | rules: [ 6 | { 7 | test: /\.js$/, 8 | exclude: /(node_modules | docs)/, 9 | use: [ 10 | { 11 | loader: "babel-loader", 12 | options: { 13 | presets: [ 14 | "@babel/preset-env", 15 | ] 16 | } 17 | } 18 | ] 19 | } 20 | ] 21 | }, 22 | target: ["web", "es5"] 23 | }; -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: "./test/index.test.js", 6 | output: { 7 | path: path.resolve(__dirname, 'test/build'), 8 | filename: 'test.js', 9 | environment: { 10 | arrowFunction: false, 11 | } 12 | } 13 | }; -------------------------------------------------------------------------------- /webpack.prd.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: "production", 5 | entry: "./src/index.dist.js", 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | filename: 'atj.min.js', 9 | environment: { 10 | arrowFunction: false, 11 | } 12 | } 13 | }; --------------------------------------------------------------------------------