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 });