github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/redux/analytics.spec.ts (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  import { assert } from "chai";
    12  import * as sinon from "sinon";
    13  
    14  import Analytics from "analytics-node";
    15  import { Location, createLocation, createHashHistory } from "history";
    16  import _ from "lodash";
    17  import { Store } from "redux";
    18  
    19  import { history } from "src/redux/state";
    20  import { AnalyticsSync, defaultRedactions } from "./analytics";
    21  import { clusterReducerObj, nodesReducerObj } from "./apiReducers";
    22  import { AdminUIState, createAdminUIStore } from "./state";
    23  
    24  import * as protos from "src/js/protos";
    25  
    26  const sandbox = sinon.createSandbox();
    27  
    28  describe("analytics listener", function() {
    29    const clusterID = "a49f0ced-7ada-4135-af37-8acf6b548df0";
    30    const setClusterData = (store: Store<AdminUIState>, enabled = true, enterprise = true) => {
    31      store.dispatch(clusterReducerObj.receiveData(
    32        new protos.cockroach.server.serverpb.ClusterResponse({
    33          cluster_id: clusterID,
    34          reporting_enabled: enabled,
    35          enterprise_enabled: enterprise,
    36        }),
    37      ));
    38    };
    39  
    40    describe("page method", function () {
    41      let store: Store<AdminUIState>;
    42      let analytics: Analytics;
    43      let pageSpy: sinon.SinonSpy;
    44  
    45      beforeEach(function () {
    46        store = createAdminUIStore(createHashHistory());
    47        pageSpy = sandbox.spy();
    48  
    49        // Analytics is a completely fake object, we don't want to call
    50        // segment if an unexpected method is called.
    51        analytics = {
    52          page: pageSpy,
    53        } as any;
    54      });
    55  
    56      afterEach(() => {
    57        sandbox.reset();
    58      });
    59  
    60      it("does nothing if cluster info is not available", function () {
    61        const sync = new AnalyticsSync(analytics, store, []);
    62  
    63        sync.page({
    64          pathname: "/test/path",
    65        } as Location);
    66  
    67        assert.isTrue(pageSpy.notCalled);
    68      });
    69  
    70      it("does nothing if reporting is not explicitly enabled", function () {
    71        const sync = new AnalyticsSync(analytics, store, []);
    72        setClusterData(store, false);
    73  
    74        sync.page({
    75          pathname: "/test/path",
    76        } as Location);
    77  
    78        assert.isTrue(pageSpy.notCalled);
    79      });
    80  
    81      it("correctly calls segment on a page call", function () {
    82        const sync = new AnalyticsSync(analytics, store, []);
    83        setClusterData(store);
    84  
    85        sync.page({
    86          pathname: "/test/path",
    87        } as Location);
    88  
    89        assert.isTrue(pageSpy.calledOnce);
    90        assert.deepEqual(pageSpy.args[0][0], {
    91          userId: clusterID,
    92          name: "/test/path",
    93          properties: {
    94            path: "/test/path",
    95            search: "",
    96          },
    97        });
    98      });
    99  
   100      it("correctly queues calls before cluster ID is available", function () {
   101        const sync = new AnalyticsSync(analytics, store, []);
   102  
   103        sync.page({
   104          pathname: "/test/path",
   105        } as Location);
   106  
   107        setClusterData(store);
   108        assert.isTrue(pageSpy.notCalled);
   109  
   110        sync.page({
   111          pathname: "/test/path/2",
   112        } as Location);
   113  
   114        assert.equal(pageSpy.callCount, 2);
   115        assert.deepEqual(pageSpy.args[0][0], {
   116          userId: clusterID,
   117          name: "/test/path",
   118          properties: {
   119            path: "/test/path",
   120            search: "",
   121          },
   122        });
   123        assert.deepEqual(pageSpy.args[1][0], {
   124          userId: clusterID,
   125          name: "/test/path/2",
   126          properties: {
   127            path: "/test/path/2",
   128            search: "",
   129          },
   130        });
   131      });
   132  
   133      it("correctly applies redaction to matched paths", function () {
   134        setClusterData(store);
   135        const sync = new AnalyticsSync(analytics, store, [
   136          {
   137            match: RegExp("/test/.*/path"),
   138            replace: "/test/[redacted]/path",
   139          },
   140        ]);
   141  
   142        sync.page({
   143          pathname: "/test/username/path",
   144        } as Location);
   145  
   146        assert.isTrue(pageSpy.calledOnce);
   147        assert.deepEqual(pageSpy.args[0][0], {
   148          userId: clusterID,
   149          name: "/test/[redacted]/path",
   150          properties: {
   151            path: "/test/[redacted]/path",
   152            search: "",
   153          },
   154        });
   155      });
   156  
   157      function testRedaction(title: string, input: string, expected: string) {
   158        return { title, input, expected };
   159      }
   160  
   161      ([
   162        testRedaction(
   163          "old database URL",
   164          "/databases/database/foobar/table/baz",
   165          "/databases/database/[db]/table/[tbl]",
   166        ),
   167        testRedaction(
   168          "new database URL",
   169          "/database/foobar/table/baz",
   170          "/database/[db]/table/[tbl]",
   171        ),
   172        testRedaction(
   173          "clusterviz map root",
   174          "/overview/map/",
   175          "/overview/map/",
   176        ),
   177        testRedaction(
   178          "clusterviz map single locality",
   179          "/overview/map/datacenter=us-west-1",
   180          "/overview/map/[locality]",
   181        ),
   182        testRedaction(
   183          "clusterviz map multiple localities",
   184          "/overview/map/datacenter=us-west-1/rack=1234",
   185          "/overview/map/[locality]/[locality]",
   186        ),
   187        testRedaction(
   188          "login redirect URL parameters",
   189          "/login?redirectTo=%2Fdatabase%2Ffoobar%2Ftable%2Fbaz",
   190          "/login?redirectTo=%2Fdatabase%2F%5Bdb%5D%2Ftable%2F%5Btbl%5D",
   191        ),
   192        testRedaction(
   193          "statement details page",
   194          "/statement/SELECT * FROM database.table",
   195          "/statement/[statement]",
   196        ),
   197      ]).map(function ({ title, input, expected }) {
   198        it(`applies a redaction for ${title}`, function () {
   199          setClusterData(store);
   200          const sync = new AnalyticsSync(analytics, store, defaultRedactions);
   201          const expectedLocation = createLocation(expected);
   202  
   203          sync.page(createLocation(input));
   204  
   205          assert.isTrue(pageSpy.calledOnce);
   206          assert.deepEqual(pageSpy.args[0][0], {
   207            userId: clusterID,
   208            name: expectedLocation.pathname,
   209            properties: {
   210              path: expectedLocation.pathname,
   211              search: expectedLocation.search,
   212            },
   213          });
   214        });
   215      });
   216    });
   217  
   218    describe("identify method", function () {
   219      let store: Store<AdminUIState>;
   220      let analytics: Analytics;
   221      let identifySpy: sinon.SinonSpy;
   222  
   223      beforeEach(function () {
   224        store = createAdminUIStore(createHashHistory());
   225        identifySpy = sandbox.spy();
   226  
   227        // Analytics is a completely fake object, we don't want to call
   228        // segment if an unexpected method is called.
   229        analytics = {
   230          identify: identifySpy,
   231        } as any;
   232      });
   233  
   234      afterEach(() => {
   235        sandbox.reset();
   236      });
   237  
   238      const setVersionData = function () {
   239        store.dispatch(nodesReducerObj.receiveData([
   240          {
   241            build_info: {
   242              tag: "0.1",
   243            },
   244          },
   245        ]));
   246      };
   247  
   248      it("does nothing if cluster info is not available", function () {
   249        const sync = new AnalyticsSync(analytics, store, []);
   250        setVersionData();
   251  
   252        sync.identify();
   253  
   254        assert.isTrue(identifySpy.notCalled);
   255      });
   256  
   257      it("does nothing if version info is not available", function () {
   258        const sync = new AnalyticsSync(analytics, store, []);
   259        setClusterData(store, true, true);
   260  
   261        sync.identify();
   262  
   263        assert.isTrue(identifySpy.notCalled);
   264      });
   265  
   266      it("does nothing if reporting is not explicitly enabled", function () {
   267        const sync = new AnalyticsSync(analytics, store, []);
   268        setClusterData(store, false, true);
   269        setVersionData();
   270  
   271        sync.identify();
   272  
   273        assert.isTrue(identifySpy.notCalled);
   274      });
   275  
   276      it("sends the correct value of clusterID, version and enterprise", function () {
   277        setVersionData();
   278  
   279        _.each([false, true], (enterpriseSetting) => {
   280          sandbox.reset();
   281          setClusterData(store, true, enterpriseSetting);
   282          const sync = new AnalyticsSync(analytics, store, []);
   283          sync.identify();
   284  
   285          assert.isTrue(identifySpy.calledOnce);
   286          assert.deepEqual(identifySpy.args[0][0], {
   287            userId: clusterID,
   288            traits: {
   289              version: "0.1",
   290              userAgent: window.navigator.userAgent,
   291              enterprise: enterpriseSetting,
   292            },
   293          });
   294        });
   295      });
   296  
   297      it("only reports once", function () {
   298        const sync = new AnalyticsSync(analytics, store, []);
   299        setClusterData(store, true, true);
   300        setVersionData();
   301  
   302        sync.identify();
   303        sync.identify();
   304  
   305        assert.isTrue(identifySpy.calledOnce);
   306      });
   307    });
   308  
   309    describe("track method", function () {
   310      const store: Store<AdminUIState> = createAdminUIStore(createHashHistory());
   311      let analytics: Analytics;
   312      let trackSpy: sinon.SinonSpy;
   313  
   314      beforeEach(() => {
   315        trackSpy = sandbox.spy();
   316  
   317        // Analytics is a completely fake object, we don't want to call
   318        // segment if an unexpected method is called.
   319        analytics = {
   320          track: trackSpy,
   321        } as any;
   322      });
   323  
   324      afterEach(() => {
   325        sandbox.reset();
   326      });
   327  
   328      it("does nothing if cluster info is not available", () => {
   329        const sync = new AnalyticsSync(analytics, store, []);
   330  
   331        sync.track({
   332          event: "test",
   333        });
   334  
   335        assert.isTrue(trackSpy.notCalled);
   336      });
   337  
   338      it("add userId to track calls using the cluster_id", () => {
   339        setClusterData(store);
   340        const sync = new AnalyticsSync(analytics, store, []);
   341  
   342        sync.track({
   343          event: "test",
   344        });
   345  
   346        const expected = {
   347          userId: clusterID,
   348          properties: {
   349            pagePath: "/",
   350          },
   351          event: "test",
   352        };
   353        const message = trackSpy.args[0][0];
   354  
   355        assert.isTrue(trackSpy.calledOnce);
   356        assert.deepEqual(message, expected);
   357      });
   358  
   359      it("add the page path to properties", () => {
   360        setClusterData(store);
   361        const sync = new AnalyticsSync(analytics, store, []);
   362        const testPagePath = "/test/page/path";
   363  
   364        history.push(testPagePath);
   365  
   366        sync.track({
   367          event: "test",
   368          properties: {
   369            testProp: "test",
   370          },
   371        });
   372  
   373        const expected = {
   374          userId: clusterID,
   375          properties: {
   376            pagePath: testPagePath,
   377            testProp: "test",
   378          },
   379          event: "test",
   380        };
   381        const message = trackSpy.args[0][0];
   382  
   383        assert.isTrue(trackSpy.calledOnce);
   384        assert.deepEqual(message, expected);
   385      });
   386    });
   387  });