├── .circleci
└── config.yml
├── .codeclimate.yml
├── .dependabot
└── config.yml
├── .eslintrc.js
├── .gitignore
├── ISSUE_TEMPLATE.md
├── LICENSE
├── README.md
├── __tests__
├── __snapshots__
│ ├── register-global-object.spec.js.snap
│ ├── router-state.spec.js.snap
│ ├── state.spec.js.snap
│ └── utils.spec.js.snap
├── api
│ ├── __snapshots__
│ │ ├── index.spec.js.snap
│ │ └── use-gtag.spec.js.snap
│ ├── config.spec.js
│ ├── custom-map.spec.js
│ ├── disable.spec.js
│ ├── event.spec.js
│ ├── exception.spec.js
│ ├── index.spec.js
│ ├── linker.spec.js
│ ├── pageview.spec.js
│ ├── purchase.spec.js
│ ├── query.spec.js
│ ├── refund.spec.js
│ ├── screenview.spec.js
│ ├── set.spec.js
│ ├── time.spec.js
│ └── use-gtag.spec.js
├── bootstrap.spec.js
├── index.spec.js
├── page-tracker.spec.js
├── register-global-object.spec.js
├── router-state.spec.js
├── state.spec.js
└── utils.spec.js
├── babel.config.js
├── bili.config.js
├── package.json
├── src
├── api
│ ├── config.js
│ ├── custom-map.js
│ ├── disable.js
│ ├── event.js
│ ├── exception.js
│ ├── index.js
│ ├── linker.js
│ ├── pageview.js
│ ├── purchase.js
│ ├── query.js
│ ├── refund.js
│ ├── screenview.js
│ ├── set.js
│ ├── time.js
│ └── use-gtag.js
├── bootstrap.js
├── index.js
├── page-tracker.js
├── register-global-object.js
├── router-state.js
├── state.js
└── utils.js
├── vue-gtag-next.d.ts
└── yarn.lock
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | jobs:
4 | dependencies:
5 | docker:
6 | - image: circleci/node:10.16.3
7 | steps:
8 | - checkout
9 | - restore_cache:
10 | key: v1-yarn-{{ checksum "yarn.lock" }}
11 | - run:
12 | command: yarn
13 | - save_cache:
14 | key: v1-yarn-{{ checksum "yarn.lock" }}
15 | paths:
16 | - ./node_modules
17 | test:
18 | docker:
19 | - image: circleci/node:10.16.3
20 | steps:
21 | - checkout
22 | - restore_cache:
23 | key: v1-yarn-{{ checksum "yarn.lock" }}
24 | - run:
25 | command: yarn test
26 |
27 | lint:
28 | docker:
29 | - image: circleci/node:10.16.3
30 | steps:
31 | - checkout
32 | - restore_cache:
33 | key: v1-yarn-{{ checksum "yarn.lock" }}
34 | - run:
35 | command: yarn lint
36 |
37 | code_climate:
38 | docker:
39 | - image: circleci/node:10.16.3
40 | steps:
41 | - checkout
42 | - restore_cache:
43 | key: v1-yarn-{{ checksum "yarn.lock" }}
44 | - run:
45 | command: |
46 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
47 | chmod +x ./cc-test-reporter
48 | ./cc-test-reporter before-build
49 | yarn coverage:lcov
50 | ./cc-test-reporter after-build
51 |
52 | build:
53 | docker:
54 | - image: circleci/node:10.16.3
55 | steps:
56 | - checkout
57 | - restore_cache:
58 | key: v1-yarn-{{ checksum "yarn.lock" }}
59 | - run:
60 | command: yarn build
61 |
62 | semantic_release:
63 | docker:
64 | - image: circleci/node:10.16.3
65 | steps:
66 | - checkout
67 | - restore_cache:
68 | key: v1-yarn-{{ checksum "yarn.lock" }}
69 | - run: yarn semantic-release
70 |
71 |
72 | workflows:
73 | version: 2
74 | workflow:
75 | jobs:
76 | - dependencies
77 |
78 | - lint:
79 | requires:
80 | - dependencies
81 |
82 | - test:
83 | requires:
84 | - dependencies
85 |
86 | - code_climate:
87 | filters:
88 | branches:
89 | only:
90 | - master
91 | requires:
92 | - test
93 | - lint
94 |
95 | - build:
96 | requires:
97 | - test
98 | - lint
99 | - code_climate
100 |
101 | - semantic_release:
102 | filters:
103 | branches:
104 | only:
105 | - master
106 | requires:
107 | - build
108 |
109 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | exclude_patterns:
2 | - "__tests__"
3 | - "!src/**/*"
4 | - "webpack.config.js"
--------------------------------------------------------------------------------
/.dependabot/config.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | update_configs:
3 | - package_manager: "javascript"
4 | directory: "/"
5 | update_schedule: "monthly"
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | "jest/globals": true
6 | },
7 | plugins: [
8 | "jest"
9 | ],
10 | extends: [
11 | "eslint:recommended",
12 | "plugin:vue/recommended",
13 | "prettier/vue",
14 | "plugin:prettier/recommended"
15 | ],
16 | parserOptions: {
17 | parser: "babel-eslint"
18 | }
19 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | .DS_Store
4 | npm-debug.log
5 | yarn-error.log
6 | coverage/
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | If you are reporting a bug, please fill in below. Otherwise feel free to remove this template entirely.
2 |
3 | ### Description
4 |
5 | What are you reporting?
6 |
7 | ### Expected behavior
8 |
9 | Tell us what you think should happen.
10 |
11 | ### Actual behavior
12 |
13 | Tell us what actually happens.
14 |
15 | ### Environment
16 |
17 | Run this command in the project folder and fill in their results:
18 |
19 | `npm ls vue-gtag`:
20 |
21 | Then, specify:
22 |
23 | 1. Operating system:
24 | 2. Browser and version:
25 |
26 | ### Reproducible Demo
27 |
28 | Please take the time to create a new app that reproduces the issue or at least some code example
29 |
30 | Demonstrable issues gets fixed faster.
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Matteo Gabriele
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Please, use vue-gtag v2.
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | # vue-gtag-next
27 |
28 | Global Site Tag plugin for Vue 3
29 |
30 | The global site tag (gtag.js) is a JavaScript tagging framework and API that allows you to send event data to Google Analytics, Google Ads, and Google Marketing Platform. For general gtag.js [documentation](https://developers.google.com/analytics/devguides/collection/gtagjs), read the gtag.js developer guide.
31 |
32 | ## Requirements
33 |
34 | Vue ^3.0.0
35 |
36 | ## Install
37 |
38 | ```bash
39 | npm install vue-gtag-next
40 | ```
41 |
42 | ## Documentation
43 |
44 | - [vue-gtag-next documentation](https://matteo-gabriele.gitbook.io/vue-gtag/v/next/)
45 | - [gtag.js official documentation](https://developers.google.com/analytics/devguides/collection/gtagjs)
46 |
47 | ## Issues and features requests
48 |
49 | Please drop an issue, if you find something that doesn't work, or a feature request at [https://github.com/MatteoGabriele/vue-gtag-next/issues](https://github.com/MatteoGabriele/vue-gtag-next/issues)
50 |
51 | Follow me on twitter [@matteo\_gabriele](https://twitter.com/matteo_gabriele) for updates
52 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/register-global-object.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`register-global-object should push arguments inside the dataLayer 1`] = `
4 | Array [
5 | Arguments [
6 | "js",
7 | 2020-01-01T01:01:01.000Z,
8 | ],
9 | Arguments [
10 | "foo",
11 | "bar",
12 | ],
13 | ]
14 | `;
15 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/router-state.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`options should render default router state 1`] = `
4 | Object {
5 | "skipSamePath": true,
6 | "template": null,
7 | "useScreenview": false,
8 | }
9 | `;
10 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/state.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`options should render default plugin state 1`] = `
4 | Object {
5 | "appId": null,
6 | "appName": null,
7 | "appVersion": null,
8 | "customResource": null,
9 | "dataLayerName": "dataLayer",
10 | "disableScriptLoader": false,
11 | "globalObjectName": "gtag",
12 | "isEnabled": true,
13 | "preconnectOrigin": "https://www.googletagmanager.com",
14 | "property": null,
15 | "resourceURL": "https://www.googletagmanager.com/gtag/js",
16 | "useDebugger": false,
17 | }
18 | `;
19 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/utils.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`loadScript should create a link for domain preconnect 1`] = `
4 |
5 |
9 |
13 |
14 | `;
15 |
16 | exports[`loadScript should create a script tag 1`] = `
17 |
18 |
22 |
23 | `;
24 |
--------------------------------------------------------------------------------
/__tests__/api/__snapshots__/index.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`api should render the api object 1`] = `
4 | Array [
5 | "config",
6 | "customMap",
7 | "disable",
8 | "event",
9 | "exception",
10 | "linker",
11 | "pageview",
12 | "purchase",
13 | "query",
14 | "refund",
15 | "screenview",
16 | "set",
17 | "time",
18 | ]
19 | `;
20 |
--------------------------------------------------------------------------------
/__tests__/api/__snapshots__/use-gtag.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`useGtag should return the api object 1`] = `
4 | Object {
5 | "bar": 2,
6 | "default": Object {
7 | "bar": 2,
8 | "foo": 1,
9 | },
10 | "foo": 1,
11 | }
12 | `;
13 |
--------------------------------------------------------------------------------
/__tests__/api/config.spec.js:
--------------------------------------------------------------------------------
1 | import config from "@/api/config";
2 | import query from "@/api/query";
3 | import state from "@/state";
4 | import { merge } from "@/utils";
5 |
6 | jest.mock("@/api/query");
7 |
8 | const defaultState = { ...state };
9 |
10 | describe("api/config", () => {
11 | beforeEach(() => {
12 | merge(state, defaultState);
13 | });
14 |
15 | afterEach(() => {
16 | jest.restoreAllMocks();
17 | jest.clearAllMocks();
18 | });
19 |
20 | it("should be called", () => {
21 | merge(state, {
22 | property: {
23 | id: 1,
24 | },
25 | });
26 |
27 | config("foo");
28 |
29 | expect(query).toHaveBeenCalledWith("config", 1, "foo");
30 |
31 | config({ a: 1 });
32 |
33 | expect(query).toHaveBeenCalledWith("config", 1, { a: 1 });
34 | });
35 |
36 | it("should be called with all properties", () => {
37 | merge(state, {
38 | property: [
39 | {
40 | id: 1,
41 | },
42 | {
43 | id: 2,
44 | },
45 | ],
46 | });
47 |
48 | config("foo");
49 |
50 | expect(query).toHaveBeenCalledTimes(2);
51 | expect(query).toHaveBeenNthCalledWith(1, "config", 1, "foo");
52 | expect(query).toHaveBeenNthCalledWith(2, "config", 2, "foo");
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/__tests__/api/custom-map.spec.js:
--------------------------------------------------------------------------------
1 | import customMap from "@/api/custom-map";
2 | import config from "@/api/config";
3 |
4 | jest.mock("@/api/config");
5 |
6 | describe("api/custom", () => {
7 | it("should be called with this parameters", () => {
8 | customMap({ foo: "bar" });
9 | expect(config).toHaveBeenCalledWith({
10 | custom_map: { foo: "bar" },
11 | });
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/__tests__/api/disable.spec.js:
--------------------------------------------------------------------------------
1 | import state from "@/state";
2 | import disable from "@/api/disable";
3 | import { merge } from "@/utils";
4 |
5 | const defaultState = { ...state };
6 |
7 | describe("ga-disable", () => {
8 | beforeEach(() => {
9 | merge(state, defaultState);
10 | });
11 |
12 | afterEach(() => {
13 | delete global["ga-disable-1"];
14 | jest.restoreAllMocks();
15 | jest.clearAllMocks();
16 | });
17 |
18 | it("should set true", () => {
19 | merge(state, {
20 | property: { id: 1 },
21 | });
22 |
23 | disable();
24 |
25 | expect(global["ga-disable-1"]).toBe(true);
26 | });
27 |
28 | it("should set false", () => {
29 | merge(state, {
30 | property: { id: 1 },
31 | });
32 |
33 | disable(false);
34 |
35 | expect(global["ga-disable-1"]).toBe(false);
36 | });
37 |
38 | it("should set false to includes domains", () => {
39 | merge(state, {
40 | property: [{ id: 1 }, { id: 2 }],
41 | });
42 |
43 | disable(false);
44 |
45 | expect(global["ga-disable-1"]).toBe(false);
46 | expect(global["ga-disable-2"]).toBe(false);
47 | });
48 |
49 | it("should not fire the event", () => {
50 | const windowSpy = jest.spyOn(global, "window", "get");
51 |
52 | windowSpy.mockImplementation(() => undefined);
53 |
54 | merge(state, {
55 | property: { id: 1 },
56 | });
57 |
58 | disable();
59 |
60 | expect(global["ga-disable-1"]).toBeUndefined();
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/__tests__/api/event.spec.js:
--------------------------------------------------------------------------------
1 | import event from "@/api/event";
2 | import query from "@/api/query";
3 | import state from "@/state";
4 | import { merge } from "@/utils";
5 |
6 | jest.mock("@/api/query");
7 |
8 | describe("api/event", () => {
9 | afterEach(() => {
10 | jest.restoreAllMocks();
11 | jest.clearAllMocks();
12 | });
13 |
14 | it("should be called with an event name and parameters", () => {
15 | event("click", { foo: "bar" });
16 |
17 | expect(query).toHaveBeenCalledWith("event", "click", { foo: "bar" });
18 | });
19 |
20 | it("should call an event for all properties", () => {
21 | merge(state, {
22 | property: [{ id: 1 }, { id: 2 }],
23 | });
24 |
25 | event("click", { foo: "bar" });
26 |
27 | expect(query).toHaveBeenCalledWith("event", "click", {
28 | foo: "bar",
29 | send_to: [1, 2],
30 | });
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/__tests__/api/exception.spec.js:
--------------------------------------------------------------------------------
1 | import exception from "@/api/exception";
2 | import event from "@/api/event";
3 |
4 | jest.mock("@/api/event");
5 |
6 | describe("api/exception", () => {
7 | it("should be called with this parameters", () => {
8 | exception("foo");
9 | expect(event).toHaveBeenCalledWith("exception", "foo");
10 |
11 | exception({ foo: "bar" });
12 | expect(event).toHaveBeenCalledWith("exception", { foo: "bar" });
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/__tests__/api/index.spec.js:
--------------------------------------------------------------------------------
1 | import * as api from "@/api";
2 |
3 | describe("api", () => {
4 | it("should render the api object", () => {
5 | expect(Object.keys(api)).toMatchSnapshot();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/__tests__/api/linker.spec.js:
--------------------------------------------------------------------------------
1 | import linker from "@/api/linker";
2 | import config from "@/api/config";
3 |
4 | jest.mock("@/api/config");
5 |
6 | describe("api/linker", () => {
7 | it("should be called with this parameters", () => {
8 | linker({ foo: "bar" });
9 | expect(config).toHaveBeenCalledWith("linker", { foo: "bar" });
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/__tests__/api/pageview.spec.js:
--------------------------------------------------------------------------------
1 | import pageview from "@/api/pageview";
2 | import event from "@/api/event";
3 |
4 | jest.mock("@/api/event");
5 |
6 | describe("api/pageview", () => {
7 | it("should be called with this parameters", () => {
8 | pageview("foo");
9 |
10 | expect(event).toHaveBeenCalledWith("page_view", {
11 | page_path: "foo",
12 | page_location: "http://localhost/",
13 | send_page_view: true,
14 | });
15 |
16 | pageview({ foo: "bar" });
17 |
18 | expect(event).toHaveBeenCalledWith("page_view", {
19 | foo: "bar",
20 | send_page_view: true,
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/__tests__/api/purchase.spec.js:
--------------------------------------------------------------------------------
1 | import purchase from "@/api/purchase";
2 | import event from "@/api/event";
3 |
4 | jest.mock("@/api/event");
5 |
6 | describe("api/purchase", () => {
7 | it("should be called with this parameters", () => {
8 | purchase("foo");
9 | expect(event).toHaveBeenCalledWith("purchase", "foo");
10 |
11 | purchase({ foo: "bar" });
12 | expect(event).toHaveBeenCalledWith("purchase", { foo: "bar" });
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/__tests__/api/query.spec.js:
--------------------------------------------------------------------------------
1 | import { merge } from "@/utils";
2 | import state from "@/state";
3 | import query from "@/api/query";
4 |
5 | const defaultState = { ...state };
6 |
7 | describe("api/query", () => {
8 | beforeEach(() => {
9 | merge(state, defaultState);
10 | });
11 |
12 | afterEach(() => {
13 | jest.restoreAllMocks();
14 | jest.clearAllMocks();
15 | });
16 |
17 | it("should call the gtag main object", () => {
18 | global.window = Object.create(window);
19 |
20 | Object.defineProperty(window, "gtag", {
21 | value: jest.fn(),
22 | });
23 |
24 | merge(state, {
25 | globalObjectName: "gtag",
26 | });
27 |
28 | query("foo", "bar");
29 |
30 | expect(global.window.gtag).toHaveBeenCalledWith("foo", "bar");
31 | });
32 |
33 | it("should not call window.gtag", () => {
34 | const windowSpy = jest.spyOn(global, "window", "get");
35 |
36 | windowSpy.mockImplementation(() => undefined);
37 |
38 | merge(state, {
39 | globalObjectName: "gtag",
40 | });
41 |
42 | query("foo", "bar");
43 |
44 | expect(global.window).toBeUndefined();
45 | });
46 |
47 | it("should log debugger events", () => {
48 | console.warn = jest.fn();
49 |
50 | merge(state, {
51 | useDebugger: true,
52 | });
53 |
54 | query("foo", "bar");
55 |
56 | expect(console.warn).toHaveBeenCalledWith("[vue-gtag] Debugger:", [
57 | "foo",
58 | "bar",
59 | ]);
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/__tests__/api/refund.spec.js:
--------------------------------------------------------------------------------
1 | import refund from "@/api/refund";
2 | import event from "@/api/event";
3 |
4 | jest.mock("@/api/event");
5 |
6 | describe("api/refund", () => {
7 | it("should be called with this parameters", () => {
8 | refund("foo");
9 | expect(event).toHaveBeenCalledWith("refund", "foo");
10 |
11 | refund({ foo: "bar" });
12 | expect(event).toHaveBeenCalledWith("refund", { foo: "bar" });
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/__tests__/api/screenview.spec.js:
--------------------------------------------------------------------------------
1 | import { merge } from "@/utils";
2 | import state from "@/state";
3 | import screenview from "@/api/screenview";
4 | import event from "@/api/event";
5 |
6 | jest.mock("@/api/event");
7 |
8 | const defaultState = { ...state };
9 |
10 | describe("api/screenview", () => {
11 | beforeEach(() => {
12 | merge(state, defaultState);
13 | });
14 |
15 | afterEach(() => {
16 | jest.clearAllMocks();
17 | });
18 |
19 | it("should be called with this parameters", () => {
20 | merge(state, {
21 | property: { id: 1 },
22 | appName: "MyApp",
23 | });
24 |
25 | screenview("bar");
26 |
27 | expect(event).toHaveBeenCalledWith("screen_view", {
28 | screen_name: "bar",
29 | app_name: "MyApp",
30 | });
31 |
32 | screenview({ foo: "bar" });
33 |
34 | expect(event).toHaveBeenCalledWith("screen_view", {
35 | foo: "bar",
36 | app_name: "MyApp",
37 | });
38 | });
39 |
40 | it("should add the app_id when defined", () => {
41 | merge(state, {
42 | appId: 123,
43 | });
44 |
45 | screenview({ foo: "bar" });
46 |
47 | expect(event).toHaveBeenCalledWith("screen_view", {
48 | foo: "bar",
49 | app_id: 123,
50 | });
51 | });
52 |
53 | it("should add the app_version when defined", () => {
54 | merge(state, {
55 | appVersion: 123,
56 | });
57 |
58 | screenview({ foo: "bar" });
59 |
60 | expect(event).toHaveBeenCalledWith("screen_view", {
61 | foo: "bar",
62 | app_version: 123,
63 | });
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/__tests__/api/set.spec.js:
--------------------------------------------------------------------------------
1 | import set from "@/api/set";
2 | import query from "@/api/query";
3 |
4 | jest.mock("@/api/query");
5 |
6 | describe("api/set", () => {
7 | it("should be called with this parameters", () => {
8 | set("foo");
9 | expect(query).toHaveBeenCalledWith("set", "foo");
10 |
11 | set({ foo: "bar" });
12 | expect(query).toHaveBeenCalledWith("set", { foo: "bar" });
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/__tests__/api/time.spec.js:
--------------------------------------------------------------------------------
1 | import time from "@/api/time";
2 | import event from "@/api/event";
3 |
4 | jest.mock("@/api/event");
5 |
6 | describe("api/time", () => {
7 | it("should be called with this parameters", () => {
8 | time("foo");
9 | expect(event).toHaveBeenCalledWith("timing_complete", "foo");
10 |
11 | time({ foo: "bar" });
12 | expect(event).toHaveBeenCalledWith("timing_complete", { foo: "bar" });
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/__tests__/api/use-gtag.spec.js:
--------------------------------------------------------------------------------
1 | import useGtag from "@/api/use-gtag";
2 |
3 | jest.mock("@/api", () => ({
4 | foo: 1,
5 | bar: 2,
6 | }));
7 |
8 | describe("useGtag", () => {
9 | it("should return the api object", () => {
10 | expect(useGtag()).toMatchSnapshot();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/__tests__/bootstrap.spec.js:
--------------------------------------------------------------------------------
1 | import state, { useState } from "@/state";
2 | import {
3 | bootstrap,
4 | isReady,
5 | useBootstrapWatcher,
6 | isBootstrapped,
7 | } from "@/bootstrap";
8 | import { merge, loadScript } from "@/utils";
9 | import flushPromises from "flush-promises";
10 | import query from "@/api/query";
11 |
12 | jest.mock("@/api/config");
13 | jest.mock("@/api/query");
14 |
15 | jest.mock("@/utils", () => {
16 | const utils = jest.requireActual("@/utils");
17 | return {
18 | ...utils,
19 | loadScript: jest.fn(() => Promise.resolve()),
20 | };
21 | });
22 |
23 | const defaultState = { ...state };
24 |
25 | describe("bootstrap", () => {
26 | beforeEach(() => {
27 | isReady.value = false;
28 | isBootstrapped.value = false;
29 | merge(state, defaultState);
30 | });
31 |
32 | afterEach(() => {
33 | jest.restoreAllMocks();
34 | jest.clearAllMocks();
35 | });
36 |
37 | it("should not load script without an id", () => {
38 | bootstrap();
39 |
40 | expect(loadScript).not.toHaveBeenCalled();
41 | });
42 |
43 | it("should not load script if already bootstrapped", () => {
44 | merge(state, {
45 | property: { id: 1 },
46 | });
47 |
48 | isBootstrapped.value = true;
49 |
50 | bootstrap();
51 |
52 | expect(loadScript).not.toHaveBeenCalled();
53 | });
54 |
55 | it("should not load script if no window is defined", () => {
56 | const windowSpy = jest.spyOn(global, "window", "get");
57 |
58 | windowSpy.mockImplementation(() => undefined);
59 |
60 | merge(state, {
61 | property: { id: 1 },
62 | });
63 |
64 | bootstrap();
65 |
66 | expect(loadScript).not.toHaveBeenCalled();
67 | });
68 |
69 | it("should not load script if no document is defined", () => {
70 | const documentSpy = jest.spyOn(global, "document", "get");
71 |
72 | documentSpy.mockImplementation(() => undefined);
73 |
74 | merge(state, {
75 | property: { id: 1 },
76 | });
77 |
78 | bootstrap();
79 |
80 | expect(loadScript).not.toHaveBeenCalled();
81 | });
82 |
83 | it("should load script", () => {
84 | merge(state, {
85 | property: { id: 1 },
86 | });
87 |
88 | bootstrap();
89 |
90 | expect(loadScript).toHaveBeenCalledWith(
91 | "https://www.googletagmanager.com/gtag/js?id=1&l=dataLayer",
92 | "https://www.googletagmanager.com"
93 | );
94 | });
95 |
96 | it("should set isReady to true once loadScript is resolved", async () => {
97 | merge(state, {
98 | property: { id: 1 },
99 | });
100 |
101 | bootstrap();
102 |
103 | await flushPromises();
104 |
105 | expect(isReady.value).toEqual(true);
106 | });
107 |
108 | it("should load a custom source", () => {
109 | merge(state, {
110 | property: { id: 1 },
111 | resourceURL: "foo.js",
112 | });
113 |
114 | bootstrap();
115 |
116 | expect(loadScript).toHaveBeenCalledWith(
117 | "foo.js?id=1&l=dataLayer",
118 | "https://www.googletagmanager.com"
119 | );
120 | });
121 |
122 | it("should fire query once bootstrapped", () => {
123 | merge(state, {
124 | property: {
125 | id: 1,
126 | params: { a: 1 },
127 | },
128 | });
129 |
130 | bootstrap();
131 |
132 | expect(query).toHaveBeenCalledWith("config", 1, {
133 | a: 1,
134 | send_page_view: false,
135 | });
136 | });
137 |
138 | it("should bootstrap multiple properties", () => {
139 | merge(state, {
140 | property: [
141 | {
142 | id: 1,
143 | params: { a: 1 },
144 | },
145 | {
146 | id: 2,
147 | default: true,
148 | params: { b: 1 },
149 | },
150 | ],
151 | });
152 |
153 | bootstrap();
154 |
155 | expect(query).toHaveBeenCalledTimes(2);
156 | expect(query).toHaveBeenNthCalledWith(1, "config", 1, {
157 | a: 1,
158 | send_page_view: false,
159 | });
160 | expect(query).toHaveBeenNthCalledWith(2, "config", 2, {
161 | b: 1,
162 | send_page_view: false,
163 | });
164 | });
165 |
166 | it("should not load the gtag api script", () => {
167 | merge(state, {
168 | disableScriptLoader: true,
169 | property: {
170 | id: "UA-1234567-8",
171 | },
172 | });
173 |
174 | bootstrap();
175 |
176 | expect(loadScript).not.toHaveBeenCalled();
177 | });
178 |
179 | it("should use the watcher to bootstrap the plugin", async () => {
180 | merge(state, {
181 | isEnabled: false,
182 | property: {
183 | id: "UA-1234567-8",
184 | },
185 | });
186 |
187 | useBootstrapWatcher();
188 |
189 | await flushPromises();
190 |
191 | expect(loadScript).not.toHaveBeenCalled();
192 |
193 | const { isEnabled } = useState();
194 |
195 | isEnabled.value = true;
196 |
197 | await flushPromises();
198 |
199 | expect(loadScript).toHaveBeenCalled();
200 | });
201 | });
202 |
--------------------------------------------------------------------------------
/__tests__/index.spec.js:
--------------------------------------------------------------------------------
1 | import plugin, {
2 | isReady,
3 | isTracking,
4 | useState,
5 | useGtag,
6 | trackRouter,
7 | } from "@/index";
8 | import { merge } from "@/utils";
9 | import registerGlobalObject from "@/register-global-object";
10 | import { useBootstrapWatcher } from "@/bootstrap";
11 |
12 | jest.mock("@/utils");
13 | jest.mock("@/bootstrap");
14 | jest.mock("@/register-global-object");
15 |
16 | describe("install", () => {
17 | it("should install the plugin", () => {
18 | const appMock = {
19 | config: {
20 | globalProperties: {},
21 | },
22 | };
23 |
24 | plugin.install(appMock);
25 |
26 | expect(merge).toHaveBeenCalled();
27 | expect(registerGlobalObject).toHaveBeenCalled();
28 | expect(useBootstrapWatcher).toHaveBeenCalled();
29 | expect(appMock.config.globalProperties.$gtag).toBeDefined();
30 | });
31 |
32 | it("should export", () => {
33 | expect(isReady).toBeDefined();
34 | expect(isTracking).toBeDefined();
35 | expect(useState).toBeDefined();
36 | expect(useGtag).toBeDefined();
37 | expect(trackRouter).toBeDefined();
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/__tests__/page-tracker.spec.js:
--------------------------------------------------------------------------------
1 | import pageview from "@/api/pageview";
2 | import screenview from "@/api/screenview";
3 | import { merge } from "@/utils";
4 | import state from "@/state";
5 | import routerState from "@/router-state";
6 | import { trackPage, trackRouter } from "@/page-tracker";
7 | import flushPromises from "flush-promises";
8 | import { ref } from "vue";
9 |
10 | jest.mock("@/api/pageview");
11 | jest.mock("@/api/screenview");
12 |
13 | const toMock = { name: "about", path: "/about" };
14 | const fromMock = { name: "home", path: "/" };
15 |
16 | const updateLocationPath = (href) => {
17 | global.window = Object.create(window);
18 |
19 | Object.defineProperty(window, "location", {
20 | value: {
21 | href,
22 | },
23 | });
24 | };
25 |
26 | const defaultState = { ...state };
27 | const defaultRouterState = { ...routerState };
28 |
29 | describe("page-tracker", () => {
30 | beforeEach(() => {
31 | merge(state, defaultState);
32 | merge(routerState, defaultRouterState);
33 | });
34 |
35 | afterEach(() => {
36 | jest.restoreAllMocks();
37 | jest.clearAllMocks();
38 | });
39 |
40 | it("should track a pageview", () => {
41 | updateLocationPath("http://localhost/about");
42 |
43 | trackPage(toMock, fromMock);
44 |
45 | expect(pageview).toHaveBeenCalledWith({
46 | page_location: "http://localhost/about",
47 | page_path: "/about",
48 | page_title: "about",
49 | });
50 | });
51 |
52 | it("should track a screenview", () => {
53 | updateLocationPath("http://localhost/about");
54 |
55 | merge(state, {
56 | appName: "MyApp",
57 | });
58 |
59 | merge(routerState, {
60 | useScreenview: true,
61 | });
62 |
63 | trackPage(toMock, fromMock);
64 |
65 | expect(screenview).toHaveBeenCalledWith({
66 | screen_name: "about",
67 | });
68 | });
69 |
70 | it("should not track when same path", () => {
71 | const to = { name: "home", path: "/" };
72 | const from = { name: "home", path: "/" };
73 |
74 | updateLocationPath("http://localhost/");
75 |
76 | trackPage(to, from);
77 |
78 | expect(pageview).not.toHaveBeenCalled();
79 | });
80 |
81 | it("should track the same path", () => {
82 | const to = { name: "home", path: "/" };
83 | const from = { name: "home", path: "/" };
84 |
85 | updateLocationPath("http://localhost/about");
86 |
87 | merge(routerState, {
88 | skipSamePath: false,
89 | });
90 |
91 | trackPage(to, from);
92 |
93 | expect(pageview).toHaveBeenCalled();
94 | });
95 |
96 | it("should return a custom template", () => {
97 | merge(routerState, {
98 | template() {
99 | return {
100 | page_title: "foo",
101 | page_path: "bar",
102 | page_location: "/foo/bar",
103 | };
104 | },
105 | });
106 |
107 | trackPage(toMock, fromMock);
108 |
109 | expect(pageview).toHaveBeenCalledWith({
110 | page_title: "foo",
111 | page_path: "bar",
112 | page_location: "/foo/bar",
113 | });
114 | });
115 |
116 | it("should start tracking when active and router is ready", async () => {
117 | updateLocationPath("http://localhost/about");
118 |
119 | const router = {
120 | isReady: jest.fn(() => Promise.resolve()),
121 | afterEach: jest.fn((fn) => fn(toMock, fromMock)),
122 | currentRoute: ref(toMock),
123 | };
124 |
125 | merge(state, {
126 | property: {
127 | id: 1,
128 | },
129 | });
130 |
131 | trackRouter(router);
132 |
133 | await flushPromises();
134 |
135 | expect(pageview).toHaveBeenCalledWith({
136 | page_title: "about",
137 | page_path: "/about",
138 | page_location: "http://localhost/about",
139 | });
140 | });
141 |
142 | it("should not start tracking if tracking is not active", async () => {
143 | const router = {
144 | isReady: jest.fn(() => Promise.resolve()),
145 | afterEach: jest.fn((fn) => fn(toMock, fromMock)),
146 | currentRoute: ref(toMock),
147 | };
148 |
149 | trackRouter(router);
150 |
151 | await flushPromises();
152 |
153 | expect(pageview).not.toHaveBeenCalled();
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/__tests__/register-global-object.spec.js:
--------------------------------------------------------------------------------
1 | import state from "@/state";
2 | import { merge } from "@/utils";
3 | import registerGlobalObject from "@/register-global-object";
4 | import mockdate from "mockdate";
5 |
6 | mockdate.set("2020-01-01T01:01:01Z");
7 |
8 | const defaultState = { ...state };
9 |
10 | describe("register-global-object", () => {
11 | beforeEach(() => {
12 | merge(state, defaultState);
13 | });
14 |
15 | afterEach(() => {
16 | delete global[state.globalObjectName];
17 | delete global[state.dataLayerName];
18 |
19 | jest.restoreAllMocks();
20 | jest.clearAllMocks();
21 | });
22 |
23 | it("should not register anything if is not a browser", () => {
24 | const windowSpy = jest.spyOn(global, "window", "get");
25 |
26 | windowSpy.mockImplementation(() => undefined);
27 |
28 | registerGlobalObject();
29 |
30 | expect(window).not.toBeDefined();
31 | });
32 |
33 | it("should register gtag global object", () => {
34 | registerGlobalObject();
35 | expect(window.gtag).toBeDefined();
36 | });
37 |
38 | it("should register gtag global object under another name", () => {
39 | merge(state, { globalObjectName: "foo" });
40 | registerGlobalObject();
41 |
42 | expect(window.foo).toBeDefined();
43 | expect(window.gtag).not.toBeDefined();
44 | });
45 |
46 | it("should push arguments inside the dataLayer", () => {
47 | registerGlobalObject();
48 |
49 | window.gtag("foo", "bar");
50 |
51 | expect(window.dataLayer).toMatchSnapshot();
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/__tests__/router-state.spec.js:
--------------------------------------------------------------------------------
1 | import state from "@/router-state";
2 | import { merge } from "@/utils";
3 |
4 | const defaultState = { ...state };
5 |
6 | describe("options", () => {
7 | beforeEach(() => {
8 | merge(state, defaultState);
9 | });
10 |
11 | afterEach(() => {
12 | jest.restoreAllMocks();
13 | jest.clearAllMocks();
14 | });
15 |
16 | it("should render default router state", () => {
17 | expect(state).toMatchSnapshot();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/__tests__/state.spec.js:
--------------------------------------------------------------------------------
1 | import state, {
2 | hasId,
3 | allProperties,
4 | isTracking,
5 | defaultProperty,
6 | } from "@/state";
7 | import { merge } from "@/utils";
8 |
9 | const defaultState = { ...state };
10 |
11 | describe("options", () => {
12 | beforeEach(() => {
13 | merge(state, defaultState);
14 | });
15 |
16 | afterEach(() => {
17 | jest.restoreAllMocks();
18 | jest.clearAllMocks();
19 | });
20 |
21 | it("should render default plugin state", () => {
22 | expect(state).toMatchSnapshot();
23 | });
24 |
25 | describe("hasId", () => {
26 | it("should return false", () => {
27 | expect(hasId.value).toEqual(false);
28 | });
29 |
30 | it("should return true", () => {
31 | merge(state, {
32 | property: {
33 | id: 1,
34 | },
35 | });
36 |
37 | expect(hasId.value).toEqual(true);
38 | });
39 | });
40 |
41 | describe("isTracking", () => {
42 | it("should return false if no properties", () => {
43 | expect(isTracking.value).toEqual(false);
44 | });
45 |
46 | it("should return true when one property is added", () => {
47 | merge(state, {
48 | property: {
49 | id: 1,
50 | },
51 | });
52 |
53 | expect(isTracking.value).toEqual(true);
54 | });
55 |
56 | it("should return false a property is added but tracking is not enabled", () => {
57 | merge(state, {
58 | isEnabled: false,
59 | property: {
60 | id: 1,
61 | },
62 | });
63 |
64 | expect(isTracking.value).toEqual(false);
65 | });
66 | });
67 |
68 | describe("defaultProperty", () => {
69 | it("should return the object", () => {
70 | merge(state, {
71 | property: {
72 | id: 1,
73 | },
74 | });
75 |
76 | expect(defaultProperty.value).toEqual({ id: 1 });
77 | });
78 |
79 | it("should return the only item in the array", () => {
80 | merge(state, {
81 | property: [
82 | {
83 | id: 1,
84 | },
85 | ],
86 | });
87 |
88 | expect(defaultProperty.value).toEqual({ id: 1 });
89 | });
90 |
91 | it("should return the only item in the array", () => {
92 | merge(state, {
93 | property: [
94 | {
95 | id: 1,
96 | },
97 | {
98 | id: 2,
99 | default: true,
100 | },
101 | ],
102 | });
103 |
104 | expect(defaultProperty.value).toEqual({ id: 2, default: true });
105 | });
106 | });
107 |
108 | describe("allProperties", () => {
109 | it("should return an array with single item", () => {
110 | merge(state, {
111 | property: {
112 | id: 1,
113 | },
114 | });
115 |
116 | expect(allProperties.value).toEqual([
117 | {
118 | id: 1,
119 | },
120 | ]);
121 | });
122 |
123 | it("should return an array with 2 items", () => {
124 | merge(state, {
125 | property: [
126 | {
127 | id: 1,
128 | },
129 | {
130 | id: 2,
131 | },
132 | ],
133 | });
134 |
135 | expect(allProperties.value).toEqual([
136 | {
137 | id: 1,
138 | },
139 | {
140 | id: 2,
141 | },
142 | ]);
143 | });
144 | });
145 | });
146 |
--------------------------------------------------------------------------------
/__tests__/utils.spec.js:
--------------------------------------------------------------------------------
1 | import * as utils from "@/utils";
2 | import flushPromises from "flush-promises";
3 |
4 | describe("loadScript", () => {
5 | afterEach(() => {
6 | document.getElementsByTagName("html")[0].innerHTML = "";
7 | });
8 |
9 | it("should return a promise", () => {
10 | expect(utils.loadScript("a")).toBeInstanceOf(Promise);
11 | });
12 |
13 | it("should create a script tag", (done) => {
14 | utils.loadScript("foo");
15 |
16 | flushPromises().then(() => {
17 | expect(document.head).toMatchSnapshot();
18 | done();
19 | });
20 | });
21 |
22 | it("should create a link for domain preconnect", (done) => {
23 | utils.loadScript("foo", "bar");
24 |
25 | flushPromises().then(() => {
26 | expect(document.head).toMatchSnapshot();
27 | done();
28 | });
29 | });
30 | });
31 |
32 | describe("merge", () => {
33 | it("should merge two objects", () => {
34 | const a = { a: 1, c: 2 };
35 | const b = { b: 1 };
36 |
37 | utils.merge(a, b);
38 |
39 | expect(a).toMatchObject({
40 | a: 1,
41 | b: 1,
42 | c: 2,
43 | });
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ["bili/babel"],
3 | };
4 |
--------------------------------------------------------------------------------
/bili.config.js:
--------------------------------------------------------------------------------
1 | import { name } from "./package.json";
2 | import path from "path";
3 |
4 | const projectRoot = path.resolve(__dirname);
5 |
6 | const config = {
7 | input: {
8 | [name]: "./src/index.js",
9 | },
10 | externals: ["vue"],
11 | plugins: {
12 | alias: {
13 | resolve: [".js"],
14 | entries: [
15 | { find: /^@\/(.*)/, replacement: path.resolve(projectRoot, "src/$1") },
16 | ],
17 | },
18 | },
19 | output: {
20 | dir: "./dist/",
21 | format: ["esm", "cjs", "umd", "umd-min"],
22 | moduleName: "VueGtag",
23 | },
24 | };
25 |
26 | export default config;
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-gtag-next",
3 | "description": "Global Site Tag (gtag.js) plugin for Vue 3",
4 | "version": "0.0.0-development",
5 | "author": {
6 | "name": "Matteo Gabriele",
7 | "email": "m.gabriele.dev@gmail.com"
8 | },
9 | "license": "MIT",
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/MatteoGabriele/vue-gtag-next"
13 | },
14 | "scripts": {
15 | "commit": "git-cz",
16 | "clean": "del-cli dist",
17 | "prebuild": "yarn clean",
18 | "build": "bili",
19 | "dev": "bili --watch",
20 | "lint": "eslint --ext .js .",
21 | "lint:fix": "yarn lint --fix",
22 | "test": "jest",
23 | "test:ci": "jest --coverage --bail --runInBand --verbose=false",
24 | "coverage": "jest --coverage",
25 | "coverage:html": "jest --coverage --coverageReporters=html",
26 | "coverage:text": "jest --coverage --coverageReporters=text",
27 | "coverage:lcov": "jest --coverage --coverageReporters=lcov",
28 | "prepublishOnly": "yarn lint && yarn test && yarn build",
29 | "semantic-release": "semantic-release"
30 | },
31 | "config": {
32 | "commitizen": {
33 | "path": "cz-conventional-changelog"
34 | }
35 | },
36 | "jest": {
37 | "collectCoverage": true,
38 | "coverageReporters": [
39 | "json",
40 | "html"
41 | ],
42 | "moduleNameMapper": {
43 | "@/(.*)$": "/src/$1"
44 | }
45 | },
46 | "keywords": [
47 | "google",
48 | "google analytics",
49 | "tracking",
50 | "google tracking",
51 | "vue-analytics",
52 | "vue-gtag",
53 | "gtag",
54 | "gtag.js",
55 | "global site tag",
56 | "vue",
57 | "vuejs"
58 | ],
59 | "main": "./dist/vue-gtag-next.js",
60 | "module": "./dist/vue-gtag-next.esm.js",
61 | "unpkg": "./dist/vue-gtag-next.umd.js",
62 | "jsdelivr": "./dist/vue-gtag-next.umd.js",
63 | "types": "./vue-gtag-next.d.ts",
64 | "files": [
65 | "dist",
66 | "vue-gtag-next.d.ts"
67 | ],
68 | "bugs": {
69 | "url": "https://github.com/MatteoGabriele/vue-gtag-next/issues"
70 | },
71 | "homepage": "https://github.com/MatteoGabriele/vue-gtag-next#readme",
72 | "peerDependencies": {
73 | "vue": "^3.0.0-rc.11"
74 | },
75 | "devDependencies": {
76 | "@babel/core": "^7.7.2",
77 | "@babel/preset-env": "^7.7.1",
78 | "babel-eslint": "^10.0.3",
79 | "bili": "^5.0.5",
80 | "commitizen": "^4.0.3",
81 | "cz-conventional-changelog": "^3.0.2",
82 | "del-cli": "^3.0.0",
83 | "eslint": "^6.6.0",
84 | "eslint-config-prettier": "^6.7.0",
85 | "eslint-plugin-jest": "^23.18.0",
86 | "eslint-plugin-prettier": "^3.1.1",
87 | "eslint-plugin-vue": "^6.0.1",
88 | "flush-promises": "^1.0.2",
89 | "jest": "^26.0.1",
90 | "mockdate": "^3.0.2",
91 | "prettier": "^2.0.5",
92 | "rollup-plugin-alias": "^2.2.0",
93 | "semantic-release": "^15.13.31",
94 | "vue": "^3.0.0-rc.11"
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/api/config.js:
--------------------------------------------------------------------------------
1 | import { allProperties } from "@/state";
2 | import query from "@/api/query";
3 |
4 | export default (...args) => {
5 | allProperties.value.forEach((property) => {
6 | query("config", property.id, ...args);
7 | });
8 | };
9 |
--------------------------------------------------------------------------------
/src/api/custom-map.js:
--------------------------------------------------------------------------------
1 | import config from "@/api/config";
2 |
3 | export default (map) => {
4 | config({
5 | custom_map: map,
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/src/api/disable.js:
--------------------------------------------------------------------------------
1 | import { isBrowser } from "@/utils";
2 | import { allProperties } from "@/state";
3 |
4 | export default (value = true) => {
5 | if (!isBrowser()) {
6 | return;
7 | }
8 |
9 | allProperties.value.forEach((property) => {
10 | window[`ga-disable-${property.id}`] = value;
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/src/api/event.js:
--------------------------------------------------------------------------------
1 | import query from "@/api/query";
2 | import { allProperties } from "@/state";
3 |
4 | export default (eventName, eventParams = {}) => {
5 | const params = { ...eventParams };
6 |
7 | if (!params.send_to && allProperties.value.length > 1) {
8 | params.send_to = allProperties.value.map((property) => property.id);
9 | }
10 |
11 | query("event", eventName, params);
12 | };
13 |
--------------------------------------------------------------------------------
/src/api/exception.js:
--------------------------------------------------------------------------------
1 | import event from "@/api/event";
2 |
3 | export default (...args) => {
4 | event("exception", ...args);
5 | };
6 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | export { default as config } from "@/api/config";
2 | export { default as customMap } from "@/api/custom-map";
3 | export { default as disable } from "@/api/disable";
4 | export { default as event } from "@/api/event";
5 | export { default as exception } from "@/api/exception";
6 | export { default as linker } from "@/api/linker";
7 | export { default as pageview } from "@/api/pageview";
8 | export { default as purchase } from "@/api/purchase";
9 | export { default as query } from "@/api/query";
10 | export { default as refund } from "@/api/refund";
11 | export { default as screenview } from "@/api/screenview";
12 | export { default as set } from "@/api/set";
13 | export { default as time } from "@/api/time";
14 |
--------------------------------------------------------------------------------
/src/api/linker.js:
--------------------------------------------------------------------------------
1 | import config from "@/api/config";
2 |
3 | export default (...args) => {
4 | config("linker", ...args);
5 | };
6 |
--------------------------------------------------------------------------------
/src/api/pageview.js:
--------------------------------------------------------------------------------
1 | import event from "@/api/event";
2 |
3 | export default (value) => {
4 | let params = {};
5 |
6 | if (typeof value === "string") {
7 | params = {
8 | page_path: value,
9 | page_location: window.location.href,
10 | };
11 | } else {
12 | params = value;
13 | }
14 |
15 | if (typeof params.send_page_view === "undefined") {
16 | params.send_page_view = true;
17 | }
18 |
19 | event("page_view", params);
20 | };
21 |
--------------------------------------------------------------------------------
/src/api/purchase.js:
--------------------------------------------------------------------------------
1 | import event from "@/api/event";
2 |
3 | export default (...args) => {
4 | event("purchase", ...args);
5 | };
6 |
--------------------------------------------------------------------------------
/src/api/query.js:
--------------------------------------------------------------------------------
1 | import { useState } from "@/state";
2 | import { isBrowser } from "@/utils";
3 |
4 | export default (...args) => {
5 | if (!isBrowser()) {
6 | return;
7 | }
8 |
9 | const { globalObjectName, useDebugger } = useState();
10 |
11 | if (useDebugger.value) {
12 | console.warn("[vue-gtag] Debugger:", args);
13 | }
14 |
15 | window[globalObjectName.value](...args);
16 | };
17 |
--------------------------------------------------------------------------------
/src/api/refund.js:
--------------------------------------------------------------------------------
1 | import event from "@/api/event";
2 |
3 | export default (...args) => {
4 | event("refund", ...args);
5 | };
6 |
--------------------------------------------------------------------------------
/src/api/screenview.js:
--------------------------------------------------------------------------------
1 | import { useState } from "@/state";
2 | import event from "@/api/event";
3 |
4 | export default (...args) => {
5 | const { appName, appId, appVersion } = useState();
6 | const [arg] = args;
7 | let params = {};
8 |
9 | if (typeof arg === "string") {
10 | params = {
11 | screen_name: arg,
12 | };
13 | } else {
14 | params = arg;
15 | }
16 |
17 | if (params.app_name == null && appName.value != null) {
18 | params.app_name = appName.value;
19 | }
20 |
21 | if (params.app_id == null && appId.value != null) {
22 | params.app_id = appId.value;
23 | }
24 |
25 | if (params.app_version == null && appVersion.value != null) {
26 | params.app_version = appVersion.value;
27 | }
28 |
29 | event("screen_view", params);
30 | };
31 |
--------------------------------------------------------------------------------
/src/api/set.js:
--------------------------------------------------------------------------------
1 | import query from "@/api/query";
2 |
3 | export default (...args) => {
4 | query("set", ...args);
5 | };
6 |
--------------------------------------------------------------------------------
/src/api/time.js:
--------------------------------------------------------------------------------
1 | import event from "@/api/event";
2 |
3 | export default (...args) => {
4 | event("timing_complete", ...args);
5 | };
6 |
--------------------------------------------------------------------------------
/src/api/use-gtag.js:
--------------------------------------------------------------------------------
1 | import * as api from "@/api";
2 |
3 | export default () => api;
4 |
--------------------------------------------------------------------------------
/src/bootstrap.js:
--------------------------------------------------------------------------------
1 | import { ref, watch } from "vue";
2 | import { loadScript, isBrowser } from "@/utils";
3 | import { query } from "@/api";
4 | import {
5 | isTracking,
6 | hasId,
7 | allProperties,
8 | defaultProperty,
9 | useState,
10 | } from "@/state";
11 |
12 | export const isReady = ref(false);
13 |
14 | export const isBootstrapped = ref(false);
15 |
16 | export const bootstrap = () => {
17 | const {
18 | disableScriptLoader,
19 | preconnectOrigin,
20 | resourceURL,
21 | dataLayerName,
22 | } = useState();
23 |
24 | if (!isBrowser() || !hasId.value || isBootstrapped.value) {
25 | return;
26 | }
27 |
28 | isBootstrapped.value = true;
29 |
30 | allProperties.value.forEach((property) => {
31 | const params = Object.assign({ send_page_view: false }, property.params);
32 | query("config", property.id, params);
33 | });
34 |
35 | if (disableScriptLoader.value) {
36 | isReady.value = true;
37 | return;
38 | }
39 |
40 | const resource = `${resourceURL.value}?id=${defaultProperty.value.id}&l=${dataLayerName.value}`;
41 |
42 | loadScript(resource, preconnectOrigin.value).then(() => {
43 | isReady.value = true;
44 | });
45 | };
46 |
47 | export const useBootstrapWatcher = () => {
48 | watch(
49 | () => isTracking.value,
50 | (val) => val && bootstrap(),
51 | { immediate: true }
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { useBootstrapWatcher } from "@/bootstrap";
2 | import { merge } from "@/utils";
3 | import state from "@/state";
4 | import * as api from "@/api";
5 | import registerGlobalObject from "@/register-global-object";
6 |
7 | export default {
8 | install: (app, newState = {}) => {
9 | merge(state, newState);
10 | registerGlobalObject();
11 | useBootstrapWatcher();
12 |
13 | app.config.globalProperties.$gtag = api;
14 | },
15 | };
16 |
17 | export { isReady } from "@/bootstrap";
18 | export { isTracking, useState } from "@/state";
19 | export { default as useGtag } from "@/api/use-gtag";
20 | export { trackRouter } from "@/page-tracker";
21 | export * from "@/api";
22 |
--------------------------------------------------------------------------------
/src/page-tracker.js:
--------------------------------------------------------------------------------
1 | import { watch, nextTick } from "vue";
2 | import { merge } from "@/utils";
3 | import { screenview, pageview } from "@/api";
4 | import { isTracking } from "@/state";
5 | import routerState, { useRouterState } from "@/router-state";
6 |
7 | export const getTemplate = (to = {}, from = {}) => {
8 | const { template, useScreenview } = useRouterState();
9 | const customTemplate = template.value ? template.value(to, from) : null;
10 |
11 | if (customTemplate) {
12 | return customTemplate;
13 | } else if (useScreenview.value) {
14 | return {
15 | screen_name: to.name,
16 | };
17 | } else {
18 | return {
19 | page_title: to.name,
20 | page_path: to.path,
21 | page_location: window.location.href,
22 | };
23 | }
24 | };
25 |
26 | export const trackPage = (to = {}, from = {}) => {
27 | const { useScreenview, skipSamePath } = useRouterState();
28 |
29 | if (skipSamePath.value && to.path === from.path) {
30 | return;
31 | }
32 |
33 | const params = getTemplate(to, from);
34 |
35 | if (useScreenview.value) {
36 | screenview(params);
37 | } else {
38 | pageview(params);
39 | }
40 | };
41 |
42 | export const trackRouter = (router, newState = {}) => {
43 | merge(routerState, newState);
44 |
45 | watch(
46 | () => isTracking.value,
47 | (val) => {
48 | if (!val) {
49 | return;
50 | }
51 |
52 | router.isReady().then(() => {
53 | nextTick(() => {
54 | trackPage(router.currentRoute.value);
55 | });
56 |
57 | router.afterEach((to, from) => {
58 | nextTick(() => {
59 | trackPage(to, from);
60 | });
61 | });
62 | });
63 | },
64 | { immediate: true }
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/src/register-global-object.js:
--------------------------------------------------------------------------------
1 | import { isBrowser } from "@/utils";
2 | import { useState } from "@/state";
3 |
4 | export default () => {
5 | if (!isBrowser()) {
6 | return;
7 | }
8 |
9 | const { globalObjectName, dataLayerName } = useState();
10 |
11 | if (window[globalObjectName.value] == null) {
12 | window[dataLayerName.value] = window[dataLayerName.value] || [];
13 | window[globalObjectName.value] = function () {
14 | window[dataLayerName.value].push(arguments);
15 | };
16 | }
17 |
18 | window[globalObjectName.value]("js", new Date());
19 | };
20 |
--------------------------------------------------------------------------------
/src/router-state.js:
--------------------------------------------------------------------------------
1 | import { toRefs, reactive } from "vue";
2 |
3 | const routerState = reactive({
4 | template: null,
5 | useScreenview: false,
6 | skipSamePath: true,
7 | });
8 |
9 | export const useRouterState = () => toRefs(routerState);
10 |
11 | export default routerState;
12 |
--------------------------------------------------------------------------------
/src/state.js:
--------------------------------------------------------------------------------
1 | import { reactive, computed, toRefs } from "vue";
2 |
3 | const state = reactive({
4 | property: null,
5 | isEnabled: true,
6 | disableScriptLoader: false,
7 | useDebugger: false,
8 | globalObjectName: "gtag",
9 | dataLayerName: "dataLayer",
10 | resourceURL: "https://www.googletagmanager.com/gtag/js",
11 | preconnectOrigin: "https://www.googletagmanager.com",
12 | customResource: null,
13 | appName: null,
14 | appId: null,
15 | appVersion: null,
16 | });
17 |
18 | export const useState = () => toRefs(state);
19 |
20 | export const defaultProperty = computed(() => {
21 | const { property } = useState();
22 |
23 | if (!property.value) {
24 | return;
25 | }
26 |
27 | if (Array.isArray(property.value)) {
28 | return property.value.find((p) => p.default === true) || property.value[0];
29 | }
30 |
31 | return property.value;
32 | });
33 |
34 | export const hasId = computed(() => {
35 | const { property } = useState();
36 |
37 | return Boolean(property.value && property.value.id !== null);
38 | });
39 |
40 | export const allProperties = computed(() => {
41 | const { property } = useState();
42 |
43 | if (Array.isArray(property.value)) {
44 | return property.value;
45 | }
46 |
47 | return [property.value];
48 | });
49 |
50 | export const isTracking = computed(() => {
51 | const { isEnabled } = useState();
52 | const property = defaultProperty.value;
53 |
54 | return Boolean(property && property.id && isEnabled.value);
55 | });
56 |
57 | export default state;
58 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export const isBrowser = () => {
2 | return typeof document !== "undefined" && typeof window !== "undefined";
3 | };
4 |
5 | export const loadScript = (source, preconnect) => {
6 | return new Promise((resolve, reject) => {
7 | const head = document.head || document.getElementsByTagName("head")[0];
8 | const script = document.createElement("script");
9 |
10 | script.async = true;
11 | script.src = source;
12 | script.charset = "utf-8";
13 |
14 | if (preconnect) {
15 | const link = document.createElement("link");
16 |
17 | link.href = preconnect;
18 | link.rel = "preconnect";
19 |
20 | head.appendChild(link);
21 | }
22 |
23 | head.appendChild(script);
24 |
25 | script.onload = resolve;
26 | script.onerror = reject;
27 | });
28 | };
29 |
30 | export const merge = (obj = {}, newObj = {}) => {
31 | Object.keys(newObj).forEach((key) => {
32 | obj[key] = newObj[key];
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/vue-gtag-next.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'vue-gtag-next' {
2 | import { Route } from 'vue-router/types/router';
3 | import VueRouter from 'vue-router';
4 | import { App, Ref } from 'vue';
5 |
6 | /**
7 | * Types copied from @types/gtag.js.
8 | *
9 | * @see https://www.npmjs.com/package/@types/gtag.js
10 | */
11 | namespace Gtag {
12 | interface Gtag {
13 | (
14 | command: 'config',
15 | targetId: string,
16 | config?: ControlParams | EventParams | CustomParams
17 | ): void;
18 | (command: 'set', config: CustomParams): void;
19 | (command: 'js', config: Date): void;
20 | (
21 | command: 'event',
22 | eventName: EventNames | string,
23 | eventParams?: ControlParams | EventParams | CustomParams
24 | ): void;
25 | }
26 |
27 | interface CustomParams {
28 | [key: string]: any;
29 | }
30 |
31 | interface ControlParams {
32 | groups?: string | string[];
33 | send_to?: string | string[];
34 | event_callback?: () => void;
35 | event_timeout?: number;
36 | }
37 |
38 | type EventNames =
39 | | 'add_payment_info'
40 | | 'add_to_cart'
41 | | 'add_to_wishlist'
42 | | 'begin_checkout'
43 | | 'checkout_progress'
44 | | 'exception'
45 | | 'generate_lead'
46 | | 'login'
47 | | 'page_view'
48 | | 'purchase'
49 | | 'refund'
50 | | 'remove_from_cart'
51 | | 'screen_view'
52 | | 'search'
53 | | 'select_content'
54 | | 'set_checkout_option'
55 | | 'share'
56 | | 'sign_up'
57 | | 'timing_complete'
58 | | 'view_item'
59 | | 'view_item_list'
60 | | 'view_promotion'
61 | | 'view_search_results';
62 |
63 | interface EventParams {
64 | checkout_option?: string;
65 | checkout_step?: number;
66 | content_id?: string;
67 | content_type?: string;
68 | coupon?: string;
69 | currency?: string;
70 | description?: string;
71 | fatal?: boolean;
72 | items?: Item[];
73 | method?: string;
74 | number?: string;
75 | promotions?: Promotion[];
76 | screen_name?: string;
77 | search_term?: string;
78 | shipping?: Currency;
79 | tax?: Currency;
80 | transaction_id?: string;
81 | value?: number;
82 | event_label?: string;
83 | event_category?: string;
84 | }
85 |
86 | type Currency = string | number;
87 |
88 | interface Item {
89 | brand?: string;
90 | category?: string;
91 | creative_name?: string;
92 | creative_slot?: string;
93 | id?: string;
94 | location_id?: string;
95 | name?: string;
96 | price?: Currency;
97 | quantity?: number;
98 | }
99 |
100 | interface Promotion {
101 | creative_name?: string;
102 | creative_slot?: string;
103 | id?: string;
104 | name?: string;
105 | }
106 | }
107 |
108 | export interface PageView {
109 | /** The page's title. */
110 | page_title?: string;
111 | /** The page's URL. */
112 | page_location?: string;
113 | /** The path portion of location. This value must start with a slash (/) character. */
114 | page_path?: string;
115 | }
116 |
117 | export interface ScreenView {
118 | /** The name of the screen. */
119 | screen_name: string;
120 | /** The name of the application. */
121 | app_name: string;
122 | }
123 |
124 | /**
125 | * @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#action_data
126 | */
127 | export interface EcommerceAction {
128 | /** Unique ID for the transaction. */
129 | transaction_id: string;
130 | /** The store or affiliation from which this transaction occurred */
131 | affiliation?: string;
132 | /** Value (i.e., revenue) associated with the event */
133 | value?: number;
134 | /** Tax amount */
135 | tax?: number;
136 | /** Shipping cost */
137 | shipping?: number;
138 | /** The array containing the associated products */
139 | items?: Gtag.Item[];
140 | /** The step (a number) in the checkout process */
141 | checkout_step?: number;
142 | /** Checkout option (i.e. selected payment method) */
143 | checkout_option?: string;
144 | }
145 |
146 | export interface Linker {
147 | domains: string[];
148 | decorate_forms?: boolean;
149 | accept_incoming?: boolean;
150 | url_position?: 'fragment' | 'query';
151 | }
152 |
153 | export interface Exception {
154 | /** A description of the error. */
155 | description?: string;
156 | /** true if the error was fatal. */
157 | fatal?: boolean;
158 | }
159 |
160 | export interface Timing {
161 | /** A string to identify the variable being recorded (e.g. 'load'). */
162 | name: string;
163 | /** The number of milliseconds in elapsed time to report to Google Analytics (e.g. 20). */
164 | value: number;
165 | /** A string for categorizing all user timing variables into logical groups (e.g. 'JS Dependencies'). */
166 | event_category?: string;
167 | /** A string that can be used to add flexibility in visualizing user timings in the reports (e.g. 'Google CDN'). */
168 | event_label?: string;
169 | }
170 |
171 | export type Dictionary = { [key: string]: T };
172 |
173 | export interface VueGtag {
174 | /**
175 | * Initialize and configure settings for a particular product account.
176 | *
177 | * @see https://developers.google.com/gtagjs/devguide/configure
178 | */
179 | config(
180 | config?: Gtag.ControlParams | Gtag.EventParams | Gtag.CustomParams
181 | ): void;
182 |
183 | /**
184 | * Configure a map of custom dimensions and metrics.
185 | *
186 | * @see https://developers.google.com/analytics/devguides/collection/gtagjs/custom-dims-mets
187 | */
188 | customMap(map: Dictionary): void;
189 |
190 | disable(value?: boolean): void;
191 |
192 | /**
193 | * Send a Google Analytics Event.
194 | *
195 | * @see https://developers.google.com/analytics/devguides/collection/gtagjs/events
196 | *
197 | * @param action string that will appear as the event action in Google Analytics Event reports
198 | * @param eventParams
199 | */
200 | event(
201 | action: Gtag.EventNames | string,
202 | eventParams?: Gtag.ControlParams | Gtag.EventParams | Gtag.CustomParams
203 | ): void;
204 |
205 | /**
206 | * Measure an exception.
207 | *
208 | * @see https://developers.google.com/analytics/devguides/collection/gtagjs/exceptions
209 | */
210 | exception(ex: Exception): void;
211 |
212 | /**
213 | * Automatically link domains.
214 | *
215 | * @see https://developers.google.com/analytics/devguides/collection/gtagjs/cross-domain#automatically_link_domains
216 | */
217 | linker(config: Linker): void;
218 |
219 | /**
220 | * Send an ad-hoc Google Analytics pageview.
221 | *
222 | * @see https://developers.google.com/analytics/devguides/collection/gtagjs/pages
223 | */
224 | pageview(pageView: PageView | string): void;
225 |
226 | /**
227 | * Measure a transaction.
228 | *
229 | * @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#measure_purchases
230 | */
231 | purchase(purchase: EcommerceAction): void;
232 |
233 | /**
234 | * Measure a full refund of a transaction.
235 | *
236 | * @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce#measure_refunds
237 | */
238 | refund(refund: EcommerceAction): void;
239 |
240 | /**
241 | * Send a Google Analytics screen view.
242 | *
243 | * @see https://developers.google.com/analytics/devguides/collection/gtagjs/screens
244 | */
245 | screenview(screenView: ScreenView): void;
246 |
247 | /**
248 | * Set parameters that will be associated with every subsequent event on the page.
249 | *
250 | * @see https://developers.google.com/gtagjs/devguide/configure#send_data_on_every_event_with_set
251 | */
252 | set(config: Gtag.CustomParams): void;
253 |
254 | /**
255 | * Send user timing information to Google Analytics.
256 | *
257 | * @see https://developers.google.com/analytics/devguides/collection/gtagjs/user-timings
258 | */
259 | time(timing: Timing): void;
260 | }
261 |
262 | export interface DomainConfig {
263 | id: string;
264 | params?: Gtag.ControlParams | Gtag.EventParams | Gtag.CustomParams;
265 | }
266 |
267 | export interface Options {
268 | property?: DomainConfig | DomainConfig[];
269 | /** defaults to true */
270 | isEnabled?: boolean;
271 | /** defaults to false */
272 | disableScriptLoader?: boolean;
273 | /** defaults to false */
274 | useDebugger?: boolean;
275 | /** defaults to 'gtag' */
276 | globalObjectName?: string;
277 | /** defaults to 'dataLayer' */
278 | dataLayerName?: string;
279 | /** defaults to 'https://www.googletagmanager.com/gtag/js' */
280 | resourceURL?: string;
281 | /** defaults to 'https://www.googletagmanager.com' */
282 | preconnectOrigin?: string;
283 | appName?: string;
284 | appId?: string;
285 | appVersion?: string;
286 | }
287 |
288 | export interface RouterOptions {
289 | template?: (to: Route, from?: Route) => PageView;
290 | /** defaults to false */
291 | useScreenview?: boolean;
292 | /** defaults to true */
293 | skipSamePath?: boolean;
294 | }
295 |
296 | export const isReady: Ref;
297 |
298 | export const isTracking: Ref;
299 |
300 | export function useState(): {
301 | [K in keyof Options]: Ref;
302 | };
303 |
304 | export function useGtag(): VueGtag;
305 |
306 | export function trackRouter(router: VueRouter, options?: RouterOptions): void;
307 |
308 | export default class VueGtagPlugin {
309 | static install(app: App, options: Options): void;
310 | }
311 |
312 | module '@vue/runtime-core' {
313 | interface ComponentCustomProperties {
314 | $gtag: VueGtag;
315 | }
316 | }
317 | }
318 |
--------------------------------------------------------------------------------