├── .gitignore ├── .travis.yml ├── AcceptableParams.md ├── HISTORY.md ├── Makefile ├── README.md ├── index.js ├── lib ├── config.js ├── index.js ├── request.js └── utils.js ├── package-lock.json ├── package.json └── test ├── _enqueue.js ├── event.js ├── exception.js ├── index.js ├── item.js ├── middleware.js ├── mocha.opts ├── pageview.js ├── send.js ├── set.js ├── timing.js └── transaction.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | node_modules 14 | 15 | npm-debug.log 16 | .idea/ 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "6" 5 | - "8" 6 | - "10" 7 | - "12" 8 | 9 | before_install: 10 | - npm install -g npm 11 | - npm conf set strict-ssl false 12 | -------------------------------------------------------------------------------- /AcceptableParams.md: -------------------------------------------------------------------------------- 1 | # Acceptable parameters 2 | 3 | 4 | ## Protocol Version 5 | 6 | Pass as: `protocolVersion` or `v` 7 | 8 | Required for all hit types.The Protocol version. The current value is '1'. This will only change when there are changes made that are not backwards compatible. 9 | 10 | 11 | ## Tracking ID / Web Property ID 12 | 13 | Pass as: `trackingId` or `webPropertyId` or `tid` 14 | 15 | Required for all hit types.The tracking ID / web property ID. The format is UA-XXXX-Y. All collected data is associated by this ID. 16 | 17 | 18 | ## Anonymize IP 19 | 20 | Pass as: `anonymizeIp` or `aip` 21 | 22 | Optional. When present, the IP address of the sender will be anonymized. For example, the IP will be anonymized if any of the following parameters are present in the payload: &aip=, &aip=0, or &aip=1 23 | 24 | 25 | ## Data Source 26 | Pass as: `dataSource` or `ds` 27 | 28 | Optional. Indicates the data source of the hit. Hits sent from analytics.js will have data source set to 'web'; hits sent from one of the mobile SDKs will have data source set to 'app'. 29 | 30 | ## Queue Time 31 | 32 | Pass as: `queueTime` or `qt` 33 | 34 | Optional. Used to collect offline / latent hits. The value represents the time delta (in milliseconds) between when the hit being reported occurred and the time the hit was sent. The value must be greater than or equal to 0. Values greater than four hours may lead to hits not being processed. 35 | 36 | 37 | ## Cache Buster 38 | 39 | Pass as: `cacheBuster` or `z` 40 | 41 | Optional. Used to send a random number in GET requests to ensure browsers and proxies don't cache hits. It should be sent as the final parameter of the request since we've seen some 3rd party internet filtering software add additional parameters to HTTP requests incorrectly. This value is not used in reporting. 42 | 43 | 44 | ## Client ID 45 | 46 | Pass as: `clientId` or `cid` 47 | 48 | Required for all hit types.This anonymously identifies a particular user, device, or browser instance. For the web, this is generally stored as a first-party cookie with a two-year expiration. For mobile apps, this is randomly generated for each particular instance of an application install. The value of this field should be a random UUID (version 4) as described in http://www.ietf.org/rfc/rfc4122.txt 49 | 50 | 51 | ## User ID 52 | 53 | Pass as: `userId` or `uid` 54 | 55 | Optional. This is intended to be a known identifier for a user provided by the site owner/tracking library user. It may not itself be PII. The value should never be persisted in GA cookies or other Analytics provided storage. 56 | 57 | 58 | ## Session Control 59 | 60 | Pass as: `sessionControl` or `sc` 61 | 62 | Optional. Used to control the session duration. A value of 'start' forces a new session to start with this hit and 'end' forces the current session to end with this hit. All other values are ignored. 63 | 64 | 65 | ## IP Override 66 | 67 | Pass as: `ipOverride` or `uip` 68 | 69 | Optional. The IP address of the user. This should be a valid IP address. It will always be anonymized just as though &aip (anonymize IP) had been used. 70 | 71 | 72 | ## User Agent Override 73 | 74 | Pass as: `userAgentOverride` or `ua` 75 | 76 | Optional. The User Agent of the browser. Note that Google has libraries to identify real user agents. Hand crafting your own agent could break at any time. 77 | 78 | 79 | ## Geographical Override 80 | 81 | Pass as: `geoid` 82 | 83 | Optional. The geographical location of the user. The geographical ID should be a two letter country code or a criteria ID representing a city or region (see http://developers.google.com/analytics/devguides/collection/protocol/v1/geoid). This parameter takes precedent over any location derived from IP address, including the IP Override parameter. An invalid code will result in geographical dimensions to be set to '(not set)'. 84 | 85 | ## Document Referrer 86 | 87 | Pass as: `documentReferrer` or `dr` 88 | 89 | Optional. Specifies which referral source brought traffic to a website. This value is also used to compute the traffic source. The format of this value is a URL. 90 | 91 | 92 | ## Campaign Name 93 | 94 | Pass as: `campaignName` or `cn` 95 | 96 | Optional. Specifies the campaign name. 97 | 98 | 99 | ## Campaign Source 100 | 101 | Pass as: `campaignSource` or `cs` 102 | 103 | Optional. Specifies the campaign source. 104 | 105 | 106 | ## Campaign Medium 107 | 108 | Pass as: `campaignMedium` or `cm` 109 | 110 | Optional. Specifies the campaign medium. 111 | 112 | 113 | ## Campaign Keyword 114 | 115 | Pass as: `campaignKeyword` or `ck` 116 | 117 | Optional. Specifies the campaign keyword. 118 | 119 | 120 | ## Campaign Content 121 | 122 | Pass as: `campaignContent` or `cc` 123 | 124 | Optional. Specifies the campaign content. 125 | 126 | 127 | ## Campaign ID 128 | 129 | Pass as: `campaignId` or `ci` 130 | 131 | Optional. Specifies the campaign ID. 132 | 133 | 134 | ## Google AdWords ID 135 | 136 | Pass as: `googleAdwordsId` or `gclid` 137 | 138 | Optional. Specifies the Google AdWords Id. 139 | 140 | 141 | ## Google Display Ads ID 142 | 143 | Pass as: `googleDisplayAdsId` or `dclid` 144 | 145 | Optional. Specifies the Google Display Ads Id. 146 | 147 | 148 | ## Screen Resolution 149 | 150 | Pass as: `screenResolution` or `sr` 151 | 152 | Optional. Specifies the screen resolution. 153 | 154 | 155 | ## Viewport size 156 | 157 | Pass as: `viewportSize` or `vp` 158 | 159 | Optional. Specifies the viewable area of the browser / device. 160 | 161 | 162 | ## Document Encoding 163 | 164 | Pass as: `documentEncoding` or `de` 165 | 166 | Optional. Specifies the character set used to encode the page / document. 167 | 168 | 169 | ## Screen Colors 170 | 171 | Pass as: `screenColors` or `sd` 172 | 173 | Optional. Specifies the screen color depth. 174 | 175 | 176 | ## User Language 177 | 178 | Pass as: `userLanguage` or `ul` 179 | 180 | Optional. Specifies the language. 181 | 182 | 183 | ## Java Enabled 184 | 185 | Pass as: `javaEnabled` or `je` 186 | 187 | Optional. Specifies whether Java was enabled. 188 | 189 | 190 | ## Flash Version 191 | 192 | Pass as: `flashVersion` or `fl` 193 | 194 | Optional. Specifies the flash version. 195 | 196 | 197 | ## Hit type 198 | 199 | Pass as: `hitType` or `t` 200 | 201 | Required for all hit types.The type of hit. Must be one of 'pageview', 'screenview', 'event', 'transaction', 'item', 'social', 'exception', 'timing'. 202 | 203 | 204 | ## Non-Interaction Hit 205 | 206 | Pass as: `non-interactionHit` or `ni` 207 | 208 | Optional. Specifies that a hit be considered non-interactive. 209 | 210 | 211 | ## Document location URL 212 | 213 | Pass as: `documentLocationUrl` or `dl` 214 | 215 | Optional. Use this parameter to send the full URL (document location) of the page on which content resides. You can use the &dh and &dp parameters to override the hostname and path + query portions of the document location, accordingly. The JavaScript clients determine this parameter using the concatenation of the document.location.origin + document.location.pathname + document.location.search browser parameters. Be sure to remove any user authentication or other private information from the URL if present. 216 | 217 | 218 | ## Document Host Name 219 | 220 | Pass as: `documentHostName` or `dh` 221 | 222 | Optional. Specifies the hostname from which content was hosted. 223 | 224 | 225 | ## Document Path 226 | 227 | Pass as: `documentPath` or `dp` 228 | 229 | Optional. The path portion of the page URL. Should begin with '/'. 230 | 231 | 232 | ## Document Title 233 | 234 | Pass as: `documentTitle` or `dt` 235 | 236 | Optional. The title of the page / document. 237 | 238 | 239 | ## Screen Name 240 | 241 | Pass as: `screenName` or `cd` 242 | 243 | Optional. If not specified, this will default to the unique URL of the page by either using the &dl parameter as-is or assembling it from &dh and &dp. App tracking makes use of this for the 'Screen Name' of the screenview hit. 244 | 245 | 246 | ## Link ID 247 | 248 | Pass as: `linkId` or `linkid` 249 | 250 | Optional. The ID of a clicked DOM element, used to disambiguate multiple links to the same URL in In-Page Analytics reports when Enhanced Link Attribution is enabled for the property. 251 | 252 | 253 | ## Application Name 254 | 255 | Pass as: `applicationName` or `an` 256 | 257 | Optional. Specifies the application name. 258 | 259 | 260 | ## Application ID 261 | 262 | Pass as: `applicationId` or `aid` 263 | 264 | Optional. Application identifier. 265 | 266 | 267 | ## Application Version 268 | 269 | Pass as: `applicationVersion` or `av` 270 | 271 | Optional. Specifies the application version. 272 | 273 | 274 | ## Application Installer ID 275 | 276 | Pass as: `applicationInstallerId` or `aiid` 277 | 278 | Optional. Application installer identifier. 279 | 280 | 281 | ## Event Category 282 | 283 | Pass as: `eventCategory` or `ec` 284 | 285 | Optional. Specifies the event category. Must not be empty. 286 | 287 | 288 | ## Event Action 289 | 290 | Pass as: `eventAction` or `ea` 291 | 292 | Optional. Specifies the event action. Must not be empty. 293 | 294 | 295 | ## Event Label 296 | 297 | Pass as: `eventLabel` or `el` 298 | 299 | Optional. Specifies the event label. 300 | 301 | 302 | ## Event Value 303 | 304 | Pass as: `eventValue` or `ev` 305 | 306 | Optional. Specifies the event value. Values must be non-negative. 307 | 308 | 309 | ## Transaction ID 310 | 311 | Pass as: `transactionId` or `ti` 312 | 313 | Required for transaction hit type.Required for item hit type.A unique identifier for the transaction. This value should be the same for both the Transaction hit and Items hits associated to the particular transaction. 314 | 315 | 316 | ## Transaction Affiliation 317 | 318 | Pass as: `transactionAffiliation` or `ta` 319 | 320 | Optional. Specifies the affiliation or store name. 321 | 322 | 323 | ## Transaction Revenue 324 | 325 | Pass as: `transactionRevenue` or `tr` 326 | 327 | Optional. Specifies the total revenue associated with the transaction. This value should include any shipping or tax costs. 328 | 329 | 330 | ## Transaction Shipping 331 | 332 | Pass as: `transactionShipping` or `ts` 333 | 334 | Optional. Specifies the total shipping cost of the transaction. 335 | 336 | 337 | ## Transaction Tax 338 | 339 | Pass as: `transactionTax` or `tt` 340 | 341 | Optional. Specifies the total tax of the transaction. 342 | 343 | 344 | ## Item Name 345 | 346 | Pass as: `itemName` or `in` 347 | 348 | Required for item hit type.Specifies the item name. 349 | 350 | 351 | ## Item Price 352 | 353 | Pass as: `itemPrice` or `ip` 354 | 355 | Optional. Specifies the price for a single item / unit. 356 | 357 | 358 | ## Item Quantity 359 | 360 | Pass as: `itemQuantity` or `iq` 361 | 362 | Optional. Specifies the number of items purchased. 363 | 364 | 365 | ## Item Code 366 | 367 | Pass as: `itemCode` or `ic` 368 | 369 | Optional. Specifies the SKU or item code. 370 | 371 | 372 | ## Item Category 373 | 374 | Pass as: `itemCategory` or `iv` 375 | 376 | Optional. Specifies the category that the item belongs to. 377 | 378 | 379 | ## Currency Code 380 | 381 | Pass as: `currencyCode` or `cu` 382 | 383 | Optional. When present indicates the local currency for all transaction currency values. Value should be a valid ISO 4217 currency code. 384 | 385 | 386 | ## Social Network 387 | 388 | Pass as: `socialNetwork` or `sn` 389 | 390 | Required for social hit type.Specifies the social network, for example Facebook or Google Plus. 391 | 392 | 393 | ## Social Action 394 | 395 | Pass as: `socialAction` or `sa` 396 | 397 | Required for social hit type.Specifies the social interaction action. For example on Google Plus when a user clicks the +1 button, the social action is 'plus'. 398 | 399 | 400 | ## Social Action Target 401 | 402 | Pass as: `socialActionTarget` or `st` 403 | 404 | Required for social hit type.Specifies the target of a social interaction. This value is typically a URL but can be any text. 405 | 406 | 407 | ## User timing category 408 | 409 | Pass as: `userTimingCategory` or `utc` 410 | 411 | Optional. Specifies the user timing category. 412 | 413 | 414 | ## User timing variable name 415 | 416 | Pass as: `userTimingVariableName` or `utv` 417 | 418 | Optional. Specifies the user timing variable. 419 | 420 | 421 | ## User timing time 422 | 423 | Pass as: `userTimingTime` or `utt` 424 | 425 | Optional. Specifies the user timing value. The value is in milliseconds. 426 | 427 | 428 | ## User timing label 429 | 430 | Pass as: `userTimingLabel` or `utl` 431 | 432 | Optional. Specifies the user timing label. 433 | 434 | 435 | ## Page Load Time 436 | 437 | Pass as: `pageLoadTime` or `plt` 438 | 439 | Optional. Specifies the time it took for a page to load. The value is in milliseconds. 440 | 441 | 442 | ## DNS Time 443 | 444 | Pass as: `dnsTime` or `dns` 445 | 446 | Optional. Specifies the time it took to do a DNS lookup. The value is in milliseconds. 447 | 448 | 449 | ## Page Download Time 450 | 451 | Pass as: `pageDownloadTime` or `pdt` 452 | 453 | Optional. Specifies the time it took for the page to be downloaded. The value is in milliseconds. 454 | 455 | 456 | ## Redirect Response Time 457 | 458 | Pass as: `redirectResponseTime` or `rrt` 459 | 460 | Optional. Specifies the time it took for any redirects to happen. The value is in milliseconds. 461 | 462 | 463 | ## TCP Connect Time 464 | 465 | Pass as: `tcpConnectTime` or `tcp` 466 | 467 | Optional. Specifies the time it took for a TCP connection to be made. The value is in milliseconds. 468 | 469 | 470 | ## Server Response Time 471 | 472 | Pass as: `serverResponseTime` or `srt` 473 | 474 | Optional. Specifies the time it took for the server to respond after the connect time. The value is in milliseconds. 475 | 476 | ## DOM Interactive Time 477 | 478 | Pass as: `domInteractiveTime` or `dit` 479 | 480 | Optional. Specifies the time it took for Document.readyState to be 'interactive'. The value is in milliseconds. 481 | 482 | ## Content Load Time 483 | 484 | Pass as: `contentLoadTime` or `clt` 485 | 486 | Optional. Specifies the time it took for the DOMContentLoaded Event to fire. The value is in milliseconds. 487 | 488 | ## Exception Description 489 | 490 | Pass as: `exceptionDescription` or `exd` 491 | 492 | Optional. Specifies the description of an exception. 493 | 494 | 495 | ## Is Exception Fatal? 496 | 497 | Pass as: `isExceptionFatal` or `exf` 498 | 499 | Optional. Specifies whether the exception was fatal. 500 | 501 | 502 | ## Custom Dimension 503 | 504 | Pass as: `cd[1-9][0-9]*` 505 | 506 | Optional. Each custom dimension has an associated index. There is a maximum of 20 custom dimensions (200 for Premium accounts). The name suffix must be a positive integer between 1 and 200, inclusive. 507 | 508 | 509 | ## Custom Metric 510 | 511 | Pass as: `cm[1-9][0-9]*` 512 | 513 | Optional. Each custom metric has an associated index. There is a maximum of 20 custom metrics (200 for Premium accounts). The name suffix must be a positive integer between 1 and 200, inclusive. 514 | 515 | 516 | ## Content Group 517 | 518 | Pass as: `cg(10|[0-9])` 519 | 520 | Optional. Up to 10 content groups are possible. 521 | 522 | 523 | ## Experiment ID 524 | 525 | Pass as: `experimentId` or `xid` 526 | 527 | Optional. This parameter specifies that this user has been exposed to an experiment with the given ID. It should be sent in conjunction with the Experiment Variant parameter. 528 | 529 | 530 | ## Experiment Variant 531 | 532 | Pass as: `experimentVariant` or `xvar` 533 | 534 | Optional. This parameter specifies that this user has been exposed to a particular variation of an experiment. It should be sent in conjunction with the Experiment ID parameter. 535 | 536 | ## Enhanced e-commerce 537 | 538 | To use enhanced e-commerce you must explicitly enable it in Google Analytics admin. Some parameters overlap with regular e-commerce tracking (`ti`, `ta`, `tr`, `tt`, `ts`), 539 | however item tracking is different. 540 | 541 | You may generate enhanced e-commerce using the [`gampee`](https://www.npmjs.com/package/gampee) module. 542 | 543 | ### Product SKU 544 | 545 | Pass as: `pr[0-9]{1,3}id` 546 | 547 | Optional. The SKU of the product. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 548 | 549 | ### Product Name 550 | 551 | Pass as: `pr[0-9]{1,3}nm` 552 | 553 | Optional. The name of the product. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 554 | 555 | ### Product Brand 556 | 557 | Pass as: `pr[0-9]{1,3}br` 558 | 559 | Optional. The brand associated with the product. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 560 | 561 | ### Product Category 562 | 563 | Pass as: `pr[0-9]{1,3}ca` 564 | 565 | Optional. The category to which the product belongs. Product index must be a positive integer between 1 and 200, inclusive. The product category parameter can be hierarchical. Use / as a delimiter to specify up to 5-levels of hierarchy. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 566 | 567 | ### Product Variant 568 | 569 | Pass as: `pr[0-9]{1,3}va` 570 | 571 | Optional. The variant of the product. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 572 | 573 | ### Product Price 574 | 575 | Pass as: `pr[0-9]{1,3}pr` 576 | 577 | Optional. The price of a product. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 578 | 579 | ### Product Quantity 580 | 581 | Pass as: `pr[0-9]{1,3}qt` 582 | 583 | Optional. The quantity of a product. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 584 | 585 | ### Product Coupon Code 586 | 587 | Pass as: `pr[0-9]{1,3}cc` 588 | 589 | Optional. The coupon code associated with a product. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 590 | 591 | ### Product Position 592 | 593 | Pass as: `pr[0-9]{1,3}ps` 594 | 595 | Optional. The product's position in a list or collection. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 596 | 597 | ### Product Custom Dimension 598 | 599 | Pass as: `pr[0-9]{1,3}cd[0-9]{1,3}` 600 | 601 | Optional. A product-level custom dimension where dimension index is a positive integer between 1 and 200, inclusive. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 602 | 603 | ### Product Custom Metric 604 | 605 | Pass as: `pr[0-9]{1,3}cm[0-9]{1,3}` 606 | 607 | Optional. A product-level custom metric where metric index is a positive integer between 1 and 200, inclusive. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 608 | 609 | ### Product Action 610 | 611 | Pass as: `pa` 612 | 613 | Optional. The role of the products included in a hit. If a product action is not specified, all product definitions included with the hit will be ignored. Must be one of: detail, click, add, remove, checkout, checkout_option, purchase, refund. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 614 | 615 | ### Transaction ID 616 | 617 | Pass as: `ti` 618 | 619 | Optional. The transaction ID. This is an additional parameter that can be sent when Product Action is set to 'purchase' or 'refund'. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 620 | 621 | ### Affiliation 622 | 623 | Pass as: `ta` 624 | 625 | Optional. The store or affiliation from which this transaction occurred. This is an additional parameter that can be sent when Product Action is set to 'purchase' or 'refund'. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 626 | 627 | ### Revenue 628 | 629 | Pass as: `tr` 630 | 631 | Optional. The total value of the transaction, including tax and shipping. If not sent, this value will be automatically calculated using the product quantity and price fields of all products in the same hit. This is an additional parameter that can be sent when Product Action is set to 'purchase' or 'refund'. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 632 | 633 | ### Tax 634 | 635 | Pass as: `tt` 636 | 637 | Optional. The total tax associated with the transaction. This is an additional parameter that can be sent when Product Action is set to 'purchase' or 'refund'. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 638 | 639 | ### Shipping 640 | 641 | Pass as: `ts` 642 | 643 | Optional. The shipping cost associated with the transaction. This is an additional parameter that can be sent when Product Action is set to 'purchase' or 'refund'. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 644 | 645 | ### Coupon Code 646 | 647 | Pass as: `tcc` 648 | 649 | Optional. The transaction coupon redeemed with the transaction. This is an additional parameter that can be sent when Product Action is set to 'purchase' or 'refund'. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 650 | 651 | ### Product Action List 652 | 653 | Pass as: `pal` 654 | 655 | Optional. The list or collection from which a product action occurred. This is an additional parameter that can be sent when Product Action is set to 'detail' or 'click'. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 656 | 657 | ### Checkout Step 658 | 659 | Pass as: `cos` 660 | 661 | Optional. The step number in a checkout funnel. This is an additional parameter that can be sent when Product Action is set to 'checkout'. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 662 | 663 | ### Checkout Step Option 664 | 665 | Pass as: `col` 666 | 667 | Optional. Additional information about a checkout step. This is an additional parameter that can be sent when Product Action is set to 'checkout'. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 668 | 669 | ### Product Impression List Name 670 | 671 | Pass as: `il[0-9]{1,3}nm` 672 | 673 | Optional. The list or collection to which a product belongs. Impression List index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 674 | 675 | ### Product Impression SKU 676 | 677 | Pass as: `il[0-9]{1,3}pi[0-9]{1,3}id` 678 | 679 | Optional. The product ID or SKU. Impression List index must be a positive integer between 1 and 200, inclusive. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 680 | 681 | ### Product Impression Name 682 | 683 | Pass as: `il[0-9]{1,3}pi[0-9]{1,3}nm` 684 | 685 | Optional. The name of the product. Impression List index must be a positive integer between 1 and 200, inclusive. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 686 | 687 | ### Product Impression Brand 688 | 689 | Pass as: `il[0-9]{1,3}pi[0-9]{1,3}br` 690 | 691 | Optional. The brand associated with the product. Impression List index must be a positive integer between 1 and 200, inclusive. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 692 | 693 | ### Product Impression Category 694 | 695 | Pass as: `il[0-9]{1,3}pi[0-9]{1,3}ca` 696 | 697 | Optional. The category to which the product belongs. Impression List index must be a positive integer between 1 and 200, inclusive. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 698 | 699 | ### Product Impression Variant 700 | 701 | Pass as: `il[0-9]{1,3}pi[0-9]{1,3}va` 702 | 703 | Optional. The variant of the product. Impression List index must be a positive integer between 1 and 200, inclusive. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 704 | 705 | ### Product Impression Position 706 | 707 | Pass as: `il[0-9]{1,3}pi[0-9]{1,3}ps` 708 | 709 | Optional. The product's position in a list or collection. Impression List index must be a positive integer between 1 and 200, inclusive. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 710 | 711 | ### Product Impression Price 712 | 713 | Pass as: `il[0-9]{1,3}pi[0-9]{1,3}pr` 714 | 715 | Optional. The price of a product. Impression List index must be a positive integer between 1 and 200, inclusive. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 716 | 717 | ### Product Impression Custom Dimension 718 | 719 | Pass as: `il[0-9]{1,3}pi[0-9]{1,3}cd[0-9]{1,3}` 720 | 721 | Optional. A product-level custom dimension where dimension index is a positive integer between 1 and 200, inclusive. Impression List index must be a positive integer between 1 and 200, inclusive. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 722 | 723 | ### Product Impression Custom Metric 724 | 725 | Pass as: `il[0-9]{1,3}pi[0-9]{1,3}cm[0-9]{1,3}` 726 | 727 | Optional. A product-level custom metric where metric index is a positive integer between 1 and 200, inclusive. Impression List index must be a positive integer between 1 and 200, inclusive. Product index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 728 | 729 | ### Promotion ID 730 | 731 | Pass as: `promo[0-9]{1,3}id` 732 | 733 | Optional. The promotion ID. Promotion index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 734 | 735 | ### Promotion Name 736 | 737 | Pass as: `promo[0-9]{1,3}nm` 738 | 739 | Optional. The name of the promotion. Promotion index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 740 | 741 | ### Promotion Creative 742 | 743 | Pass as: `promo[0-9]{1,3}cr` 744 | 745 | Optional. The creative associated with the promotion. Promotion index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 746 | 747 | ### Promotion Position 748 | 749 | Pass as: `promo[0-9]{1,3}ps` 750 | 751 | Optional. The position of the creative. Promotion index must be a positive integer between 1 and 200, inclusive. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 752 | 753 | ### Promotion Action 754 | 755 | Pass as: `promoa` 756 | 757 | Optional. Specifies the role of the promotions included in a hit. If a promotion action is not specified, the default promotion action, 'view', is assumed. To measure a user click on a promotion set this to 'promo_click'. For analytics.js the Enhanced Ecommerce plugin must be installed before using this field. 758 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ## 0.5.3 4 | 5 | - Refactored request sending to no use JSON.stringify 6 | - Removed chapter regarding requestOptions that are no longer available since 0.5 7 | - Fixed UUID test errors 8 | 9 | ## 0.5.2 10 | 11 | - Upgraded debug to 4.3.1 12 | - Improved documentation 13 | 14 | ## 0.5.1 15 | 16 | - Fixed bug that caused requests to fail 17 | 18 | ## 0.5 19 | 20 | - Removed request dependency 21 | - Updated Readme 22 | - Added nodejs 10 and 12 in travis tests 23 | - Option to override default instance name 24 | - Invalid configuration key in README file. 25 | 26 | ## 0.4.22 27 | 28 | - Pin native-request version to 1.0.5 29 | 30 | ## 0.4.21 31 | 32 | - Upgraded request to native-request 33 | 34 | ## 0.4.20 35 | 36 | - Maintenance release 37 | 38 | ## 0.4.19 39 | 40 | - Updated request package 41 | 42 | ## 0.4.18 43 | 44 | - Fixed package-lock.json bug 45 | 46 | ## 0.4.17 47 | 48 | - Default to `https` instead of `http` when submitting data to Google Analytics 49 | - Switched from custom debugger to [debug](https://www.npmjs.com/package/debug) module 50 | 51 | Deprecated: 52 | - `.debug()` is now deprecated in favor of setting the DEBUG environment variable: `DEBUG=universal-analytics` 53 | 54 | ## 0.4.16 55 | 56 | - Removed async, underscore dependencies 57 | 58 | ## 0.4.15 / 2017-08-10 59 | 60 | - Fixed dependencies 61 | 62 | ## 0.4.14 / 2017-08-07 63 | 64 | - Updated AcceptableParams.md 65 | - Updated .travis.yml to test Node.js up to 8 66 | - Updated package.json to be more specific about Sinon 67 | 68 | ## 0.4.13 / 2017-03-29 69 | 70 | - Added `screenview` method 71 | - Updated Readme 72 | 73 | ## 0.4.12 / 2017-03-16 74 | 75 | - Fix `pageview` key mappings 76 | 77 | ## 0.4.11 / 2017-03-14 78 | 79 | - Updated UA#event to translate parameters before merging with persisted params 80 | - Added parameter translation to all other tracking calls 81 | 82 | ## 0.4.10 / 2017-03-06 83 | 84 | - Added content groups as acceptable params 85 | 86 | ## 0.4.9 / 2017-02-17 87 | 88 | - Readme update 89 | 90 | ## 0.4.8 / 2016-11-23 91 | 92 | - Added support for providing request options 93 | 94 | ## 0.4.7 / 2016-11-23 95 | 96 | - Updated uuid module to 3.0.0 97 | 98 | ## 0.4.5 / 2016-09-14 99 | 100 | - Fixed bug caused by different should.js version 101 | 102 | ## 0.4.4 / 2016-09-14 103 | 104 | - Fixed bug in #set() method 105 | 106 | ## 0.4.3 / 2016-09-14 107 | 108 | - Added #set() method for persistent parameters 109 | 110 | ## 0.4.2 / 2016-07-22 111 | 112 | - Added batch requests 113 | 114 | ## 0.3.11 / 2016-03-16 115 | 116 | - Added requestOptions to allow custom modifications for the request call 117 | 118 | ## 0.3.10 / 2015-12-18 119 | 120 | - Fixed typo in the error handler for events 121 | - Translate event parameters before validation 122 | 123 | ## 0.3.9 / 2015-07-20 124 | 125 | - Added possibility of overwrite the hostname of google analytics to HTTPS 126 | 127 | ## 0.3.7 / 2015-05-18 128 | 129 | - Include data as application/x-www-form-urlencoded - allows 8kb of data instead of the 2kb from query string 130 | - Fixes #25: Accepting Enhanced Ecommerce params (and newly added MP params) without warnings 131 | - Accept document location instead of page path 132 | 133 | ## 0.3.5 / 2014-10-22 134 | 135 | - Added parameter translation 136 | 137 | ## 0.3.4 / 2014-02-28 138 | 139 | - Return number of requests sent to GA for send() callback 140 | 141 | ## 0.3.2 / 2014-01-09 142 | 143 | - Fixed Travis build 144 | 145 | ## 0.3.1 / 2014-01-06 146 | 147 | - Added option to disable strict CID enforcement 148 | 149 | ## 0.3 / 2013-09-09 150 | 151 | - Added .middleware() and.createFromSession() methods for better session-based identification 152 | - Allow custom http headers to be POSTed to UA 153 | 154 | ## 0.2.2 / 2013-04-16 155 | 156 | - Updated repository URL 157 | 158 | ## 0.2.1 / 2013-04-08 159 | 160 | - Fixed bug that caused (params, fn) signature of #exception and #timing to not work 161 | - Updated documentation 162 | 163 | ## 0.2 / 2013-03-26 164 | 165 | - Added Timing and Exceptions 166 | - Fixed some invalid space characters 167 | 168 | ## 0.1 / 2013-02-11 169 | 170 | - Initial version 171 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | ./node_modules/.bin/mocha 4 | 5 | .PHONY: test 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | universal-analytics 2 | ======= 3 | 4 | Setting up a new property in Google Analytics? follow these instructions to get your UA-xxxx code. 5 | https://support.google.com/analytics/answer/9304153#UA 6 | 7 | 8 | A node module for Google's [Universal Analytics](http://support.google.com/analytics/bin/answer.py?hl=en&hlrm=de&answer=2790010) tracking via the [Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/v1/). 9 | 10 | This module allows tracking data (or rather, users) from within a Node.js application. Tracking is initiated on the server side and, if required, does not require any more tracking in the browser. 11 | 12 | [![npm version](https://badge.fury.io/js/universal-analytics.svg)](https://www.npmjs.com/package/universal-analytics) [![Build Status](https://travis-ci.org/peaksandpies/universal-analytics.png?branch=master)](https://travis-ci.org/peaksandpies/universal-analytics) 13 | 14 | 15 | # Table of Contents 16 | 17 | - [Getting started](#getting-started) 18 | - [Tracking](#tracking) 19 | - [Pageviews](#pageview-tracking) 20 | - [Screenviews](#screenview-tracking) 21 | - [Events](#event-tracking) 22 | - [Exceptions](#exception-tracking) 23 | - [User timings](#user-timing-tracking) 24 | - [Transactions](#transaction-tracking) 25 | - [Transaction items](#transaction-item-tracking) 26 | - [Daisy-chaining tracking calls](#daisy-chaining-tracking-calls) 27 | - [Setting persistent parameters](#setting-persistent-parameters) 28 | - [Filter application tracking data](#filter-application-tracking-data) 29 | - [Session-based identification](#session-based-identification) 30 | - [Debug mode](#debug-mode) 31 | - [Request Options](#request-options) 32 | - [Shortcuts](#shortcuts) 33 | - [Tests](#tests) 34 | 35 | 36 | # Getting started 37 | 38 | `universal-analytics` is installed and included like any other node module: 39 | 40 | ``` 41 | $ npm install universal-analytics 42 | ``` 43 | 44 | ```javascript 45 | var ua = require('universal-analytics'); 46 | 47 | // Or with ES6 import 48 | import ua from 'universal-analytics' 49 | ``` 50 | 51 | Initialization expects at least your Google Analytics account ID: 52 | 53 | ```javascript 54 | var visitor = ua('UA-XXXX-XX'); 55 | ``` 56 | 57 | This will create a `universal-analytics` Visitor instance that you can use and keep around to track a specific client (Not to be confused with the Google Analytics User ID, see [Setting persistent parameters](#setting-persistent-parameters) for more information on that). Since no client ID was specified in the constructor's arguments, a random UUID is generated. In case you have a client ID at hand, you can use that to create the visitor: 58 | 59 | ```javascript 60 | var visitor = ua('UA-XXXX-XX', '6a14abda-6b12-4578-bf66-43c754eaeda9'); 61 | ``` 62 | 63 | Starting with Universal Analytics, a UUID v4 is the preferred client ID format. It is therefor necessary to provide a UUID of such type to `universal-analytics`. However you can force custom client ID, passing `strictCidFormat: false` in the options: 64 | 65 | ```javascript 66 | var visitor = ua('UA-XXXX-XX', 'CUSTOM_CLIENTID_1', { strictCidFormat: false }); 67 | ``` 68 | 69 | If you want to force the HTTP protocol instead of HTTPS, include `http: true` in the options, by default this module will use https: 70 | ```javascript 71 | var visitor = ua('UA-XXXX-XX', { http: true }); 72 | ``` 73 | 74 | If you want to set User Id you can add it into options: 75 | ```javascript 76 | var visitor = ua('UA-XXXX-XX', { uid: 'as8eknlll'}); 77 | ``` 78 | [see about User Id](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#uid) 79 | 80 | 81 | Tracking a pageview without much else is now very simple: 82 | 83 | ```javascript 84 | visitor.pageview("/").send() 85 | ``` 86 | 87 | The first argument for the pageview method is the path of the page to be tracked. Simply calling `pageview()` will not initiate a tracking request. In order to send off tracking to the Google Analytics servers you have two options: 88 | 89 | 1. You can append a `send()` call after `pageview()`. The tracking request is sent asynchronously. This means you will not receive any confirmation when and if it was successful. 90 | 2. You can provide a callback function to `pageview()` as an additional, last argument. This callback will be invoked once the tracking request has finished. Any error that occured during the request will be provided to said callback. In that case `send()` is no longer necessary. 91 | 92 | An example of the callback approach: 93 | 94 | ```javascript 95 | var visitor = ua('UA-XXXX-XX'); 96 | visitor.pageview("/", function (err) { 97 | // Handle the error if necessary. 98 | // In case no error is provided you can be sure 99 | // the request was successfully sent off to Google. 100 | }); 101 | ``` 102 | 103 | 104 | 105 | # Tracking 106 | 107 | 108 | ## Pageview tracking 109 | 110 | The first argument for the pageview tracking call is the page path. Furthermore, pageview tracking can be improved with the additional parameters to provide the page's hostname and title to Google Analytics. The parameters are provided as arguments after the page path. 111 | 112 | ```javascript 113 | visitor.pageview("/", "http://peaksandpies.com", "Welcome").send(); 114 | ``` 115 | 116 | The following snippet is the exact same tracking using a callback. It is always the last argument. 117 | 118 | ```javascript 119 | visitor.pageview("/", "http://peaksandpies.com", "Welcome", function (err) { 120 | // … 121 | }); 122 | ``` 123 | 124 | Depending on how you integrate tracking into your app, you might be more comfortable with providing all the tracking data via a params object to the `pageview()` method: 125 | 126 | ```javascript 127 | visitor.pageview({dp: "/", dt: "Welcome", dh: "http://peaksandpies.com"}).send(); 128 | ``` 129 | 130 | This code has the exact same effect as the one above. `dp`, `dt`, and `dh` (as in 'document path', 'document title' and 'document hostname') are the attribute names used by the Measurement Protocol. 131 | 132 | It's mandatory to specify either the page path (`dp`) or document location (`dl`). Google Analytics can not track a pageview without a path. To avoid such erroneous requests, `universal-analytics` will deny `pageview()` tracking if the required parameters are omitted. 133 | 134 | ```javascript 135 | var pagePath = null; 136 | 137 | visitor.pageview(pagePath, function (err) { 138 | // This callback will receive an error 139 | }); 140 | ``` 141 | 142 | The following method signatures are available for the `pageview()` method of the Visitor instance: 143 | 144 | * `Visitor#pageview(path)` 145 | * `Visitor#pageview(path, callback)` 146 | * `Visitor#pageview(params)` 147 | * `Visitor#pageview(params, callback)` 148 | * `Visitor#pageview(path, hostname)` 149 | * `Visitor#pageview(path, hostname, callback)` 150 | * `Visitor#pageview(path, hostname, title)` 151 | * `Visitor#pageview(path, hostname, title, callback)` 152 | 153 | See also: [List of acceptable params](AcceptableParams.md). 154 | 155 | 156 | 157 | 158 | 159 | ## Screenview tracking 160 | 161 | Instead of pageviews app will want to track screenviews. 162 | 163 | ```javascript 164 | visitor.screenview("Home Screen", "App Name").send() 165 | ``` 166 | 167 | The following method signatures are available for #screenview: 168 | 169 | * `Visitor#screenview(screenName, appName)` 170 | * `Visitor#screenview(screenName, appName, callback)` 171 | * `Visitor#screenview(screenName, appName, appVersion)` 172 | * `Visitor#screenview(screenName, appName, appVersion, callback)` 173 | * `Visitor#screenview(screenName, appName, appVersion, appId)` 174 | * `Visitor#screenview(screenName, appName, appVersion, appId, callback)` 175 | * `Visitor#screenview(screenName, appName, appVersion, appId, appInstallerId)` 176 | * `Visitor#screenview(screenName, appName, appVersion, appId, appInstallerId, callback)` 177 | * `Visitor#screenview(screenName, appName, appVersion, appId, appInstallerId, params)` 178 | * `Visitor#screenview(screenName, appName, appVersion, appId, appInstallerId, params, callback)` 179 | * `Visitor#screenview(params)` 180 | * `Visitor#screenview(params, callback)` 181 | 182 | See also: [List of acceptable params](AcceptableParams.md). 183 | 184 | 185 | 186 | 187 | 188 | 189 | ## Event tracking 190 | 191 | 192 | Tracking events with `universal-analytics` works like pageview tracking, only you have to provide different arguments: 193 | 194 | ```javascript 195 | visitor.event("Event Category", "Event Action").send() 196 | ``` 197 | 198 | This is the most straightforward way to track an event. The event attributes *label* and *value* are optional and can be provided if necessary: 199 | 200 | ```javascript 201 | visitor.event("Event Category", "Event Action", "…and a label", 42).send() 202 | ``` 203 | 204 | Just like pageview tracking, event tracking supports a callback as the last argument: 205 | 206 | ```javascript 207 | visitor.event("Event Category", "Event Action", "…and a label", 42, function (err) { 208 | // … 209 | }) 210 | ``` 211 | 212 | An additional attribute for events is the path of the page they should be associated with in Google Analytics. You can provide this path via an additional params object: 213 | 214 | ```javascript 215 | visitor.event("Event Category", "Event Action", "…and a label", 42, {p: "/contact"}, function (err) { 216 | // … 217 | }) 218 | ``` 219 | 220 | *Notice:* The page path attribute for the event is called `p` which differs from the `dp` attribute used in the pageview tracking example. `universal-analytics` is smart enough to use the `dp` attribute should you provide it instead of `p`. 221 | 222 | In case this argument list is getting a little long, `event()` also accepts a params object like `pageview()`: 223 | 224 | ```javascript 225 | var params = { 226 | ec: "Event Category", 227 | ea: "Event Action", 228 | el: "…and a label", 229 | ev: 42, 230 | dp: "/contact" 231 | } 232 | 233 | visitor.event(params).send(); 234 | ``` 235 | 236 | The category (`ec`) and the action (`ea`) are mandatory. Google Analytics will not track an event without them. To avoid such erroneous requests, universal-analytics will deny `event()` tracking if either attribute is omitted. 237 | 238 | ```javascript 239 | var action = null; 240 | 241 | visitor.event("Navigation clicks", action, function (err) { 242 | // This callback will receive an error 243 | }); 244 | ``` 245 | 246 | The following method signatures are available for #event: 247 | 248 | * `Visitor#event(category, action)` 249 | * `Visitor#event(category, action, callback)` 250 | * `Visitor#event(category, action, label)` 251 | * `Visitor#event(category, action, label, callback)` 252 | * `Visitor#event(category, action, label, value)` 253 | * `Visitor#event(category, action, label, value, callback)` 254 | * `Visitor#event(category, action, label, value, params, callback)` 255 | * `Visitor#event(params)` 256 | * `Visitor#event(params, callback)` 257 | 258 | See also: [List of acceptable params](AcceptableParams.md). 259 | 260 | 261 | 262 | 263 | 264 | ## E-commerce tracking 265 | 266 | E-commerce tracking in general is a bit more complex. It requires a combination of one call to the `transaction()` method and one or more calls to the `item()` method. 267 | 268 | ```javascript 269 | visitor 270 | .transaction("trans-12345", 500) // Create transaction trans-12345 worth 500 total. 271 | .item(300, 1, "item-54321") // Add 1 unit the item item-54321 worth 300. 272 | .item(200, 2, "item-41325") // Add 2 units the item item-41325 worth 200. 273 | .send() 274 | ``` 275 | 276 | Once again, daisy-chaining simplifies associating the items with the transaction. Officially, nothing but the transaction ID is a requirement for both the transaction and the items. However, providing a minimum set of information (revenue for the transaction, price, quantity and ID for the items) is recommended. 277 | 278 | It is also possible to provide the params as an object to both methods: 279 | 280 | ```javascript 281 | visitor 282 | .transaction({ti: "trans-12345", tr: 500, ts: 50, tt: 100, ta: "Partner 13"}) 283 | .item({ip: 300, iq: 1, ic: "item-54321", in: "Item 54321", iv: "Blue"}) 284 | .item({ip: 200, iq: 2, ic: "item-41325", in: "Item 41325", iv: "XXL"}) 285 | .send() 286 | ``` 287 | 288 | In case an additional item has to be added later on or daisy-chaining is not available for another reason, each item can be given an associated transaction ID via the params object as well: 289 | 290 | visitor.item({ip: 100, iq: 1, ic: "item-41325", in: "Item 41325", iv: "XL", ti: "trans-12345"}).send() 291 | 292 | The transaction ID (`ti`) is mandatory for both the transaction and the item. Google Analytics will not track e-commerce data without it. To avoid such erroneous requests, universal-analytics will deny `transaction()` and `item()` tracking if it is omitted. 293 | 294 | ```javascript 295 | var ti = null; 296 | 297 | visitor.transaction(ti, function (err) { 298 | // This callback will receive an error 299 | }); 300 | ``` 301 | 302 | The following method signatures are available for #transaction: 303 | 304 | * `Visitor#transaction(id)` 305 | * `Visitor#transaction(id, callback)` 306 | * `Visitor#transaction(id, revenue)` 307 | * `Visitor#transaction(id, revenue, callback)` 308 | * `Visitor#transaction(id, revenue, shipping)` 309 | * `Visitor#transaction(id, revenue, shipping, callback)` 310 | * `Visitor#transaction(id, revenue, shipping, tax)` 311 | * `Visitor#transaction(id, revenue, shipping, tax, callback)` 312 | * `Visitor#transaction(id, revenue, shipping, tax, affiliation)` 313 | * `Visitor#transaction(id, revenue, shipping, tax, affiliation, callback)` 314 | * `Visitor#transaction(params)` 315 | * `Visitor#transaction(params, callback)` 316 | 317 | The following method signatures are available for #item: 318 | 319 | * `Visitor#item(price)` 320 | * `Visitor#item(price, callback)` 321 | * `Visitor#item(price, quantity)` 322 | * `Visitor#item(price, quantity, callback)` 323 | * `Visitor#item(price, quantity, sku)` 324 | * `Visitor#item(price, quantity, sku, callback)` 325 | * `Visitor#item(price, quantity, sku, name)` 326 | * `Visitor#item(price, quantity, sku, name, callback)` 327 | * `Visitor#item(price, quantity, sku, name, variation)` 328 | * `Visitor#item(price, quantity, sku, name, variation, callback)` 329 | * `Visitor#item(price, quantity, sku, name, variation, params)` 330 | * `Visitor#item(price, quantity, sku, name, variation, params, callback)` 331 | * `Visitor#item(params)` 332 | * `Visitor#item(params, callback)` 333 | 334 | See also: [List of acceptable params](AcceptableParams.md). 335 | 336 | 337 | 338 | 339 | 340 | 341 | ## Exception tracking 342 | 343 | Exception tracking is a way to keep track of any sort of application errors and bugs with Google Analytics. Using it with this module is a way to capture server-side problems. 344 | 345 | ```javascript 346 | visitor.exception("StackOverflow Error").send() 347 | ``` 348 | 349 | As an additional information, the exception can be flagged as fatal if the error was exceptionally bad. 350 | 351 | ```javascript 352 | var fatal = true; 353 | visitor.exception("StackOverflow Error", fatal, function () { 354 | // Finish handling this error 355 | }); 356 | ``` 357 | 358 | The following method signatures are available for #exception: 359 | 360 | * `Visitor#exception(description)` 361 | * `Visitor#exception(description, callback)` 362 | * `Visitor#exception(description, fatal)` 363 | * `Visitor#exception(description, fatal, callback)` 364 | * `Visitor#exception(params)` 365 | * `Visitor#exception(params, callback)` 366 | 367 | See also: [List of acceptable params](AcceptableParams.md). 368 | 369 | 370 | 371 | 372 | 373 | 374 | ## User timing tracking 375 | 376 | Tracking user timings is a way to capture time-based information similar to the page load speed data tracked automatically by Google Analytics. All arguments to this tracking method are optional, but a category, a variable and a time value should be provided. The time value should be provided in milliseconds. 377 | 378 | ```javascript 379 | visitor.timing("User interaction", "Time to open login overlay", 12547).send() 380 | ``` 381 | 382 | The following method signatures are available for #timing: 383 | 384 | * `Visitor#timing(category)` 385 | * `Visitor#timing(category, callback)` 386 | * `Visitor#timing(category, variable)` 387 | * `Visitor#timing(category, variable, callback)` 388 | * `Visitor#timing(category, variable, time)` 389 | * `Visitor#timing(category, variable, time, callback)` 390 | * `Visitor#timing(category, variable, time, label)` 391 | * `Visitor#timing(category, variable, time, label, callback)` 392 | * `Visitor#timing(params)` 393 | * `Visitor#timing(params, callback)` 394 | 395 | See also: [List of acceptable params](AcceptableParams.md). 396 | 397 | 398 | 399 | 400 | 401 | ## Transaction tracking 402 | 403 | Transactions are the main tracking calls for ecommerce tracking 404 | 405 | ```javascript 406 | visitor.transaction("123456", "449.99").send() 407 | ``` 408 | 409 | The following method signatures are available for #transaction: 410 | 411 | * `Visitor#transaction(transactionId)` 412 | * `Visitor#transaction(transactionId, callback)` 413 | * `Visitor#transaction(transactionId, revenue)` 414 | * `Visitor#transaction(transactionId, revenue, callback)` 415 | * `Visitor#transaction(transactionId, revenue, shippingCost)` 416 | * `Visitor#transaction(transactionId, revenue, shippingCost, callback)` 417 | * `Visitor#transaction(transactionId, revenue, shippingCost, tax)` 418 | * `Visitor#transaction(transactionId, revenue, shippingCost, tax, callback)` 419 | * `Visitor#transaction(transactionId, revenue, shippingCost, tax, affiliation)` 420 | * `Visitor#transaction(transactionId, revenue, shippingCost, tax, affiliation, callback)` 421 | * `Visitor#transaction(transactionId, revenue, shippingCost, tax, affiliation, params)` 422 | * `Visitor#transaction(transactionId, revenue, shippingCost, tax, affiliation, params, callback)` 423 | * `Visitor#transaction(params)` 424 | * `Visitor#transaction(params, callback)` 425 | 426 | See also: [List of acceptable params](AcceptableParams.md). 427 | 428 | 429 | 430 | 431 | 432 | ### Transaction item tracking 433 | 434 | Transaction consist of one or more items. 435 | 436 | ```javascript 437 | visitor.item(449.99, 1, "ID54321", "T-Shirt", {ti: "123456"}).send() 438 | ``` 439 | 440 | The following method signatures are available for #item: 441 | 442 | * `Visitor#item(price)` 443 | * `Visitor#item(price, callback)` 444 | * `Visitor#item(price, quantity)` 445 | * `Visitor#item(price, quantity, callback)` 446 | * `Visitor#item(price, quantity, sku)` 447 | * `Visitor#item(price, quantity, sku, callback)` 448 | * `Visitor#item(price, quantity, sku, name)` 449 | * `Visitor#item(price, quantity, sku, name, callback)` 450 | * `Visitor#item(price, quantity, sku, name, variation)` 451 | * `Visitor#item(price, quantity, sku, name, variation, callback)` 452 | * `Visitor#item(price, quantity, sku, name, variation, params)` 453 | * `Visitor#item(price, quantity, sku, name, variation, params, callback)` 454 | * `Visitor#item(params)` 455 | * `Visitor#item(params, callback)` 456 | 457 | See also: [List of acceptable params](AcceptableParams.md). 458 | 459 | 460 | 461 | 462 | 463 | 464 | # Daisy-chaining tracking calls 465 | 466 | We have seen basic daisy-chaining above when calling `send()` right after `pageview()` and `event()`: 467 | 468 | ```javascript 469 | visitor.pageview("/").send() 470 | ``` 471 | 472 | Every call of a tracking method returns a visitor instance you can re-use: 473 | 474 | ```javascript 475 | visitor.pageview("/").pageview("/contact").send() 476 | ``` 477 | 478 | Granted, the chance of this example actually happening in practice might be rather low. 479 | 480 | However, `universal-analytics` is smart when it comes to daisy-chaining certain calls. In many cases, a `pageview()` call is instantly followed by an `event()` call to track some additional information about the current page. `universal-analytics` makes creating the connection between the two easy: 481 | 482 | ```javascript 483 | visitor.pageview("/landing-page-1").event("Testing", "Button color", "Blue").send() 484 | ``` 485 | This is the same as two distinct tracking calls. 486 | 487 | ```javascript 488 | visitor.pageview("/landing-page-1").send() 489 | visitor.event("Testing", "Button color", "Blue", {p: "/landing-page-1"}).send() 490 | ``` 491 | 492 | Daisy-chaining is context-aware and in this case placing the `event()` call right after the `pageview()` call results in the event being associated with the page path tracking in the `pageview()` call. Even though the attributes (`dp` and `p`) are different internally. 493 | 494 | It also works when using a callback since the `this` inside the callback will be the `universal-analytics` Visitor instance: 495 | 496 | ```javascript 497 | visitor.pageview("/landing-page-1", function (err) { 498 | if (!err) { 499 | this.event("Testing", "Button color", "Blue").send() 500 | } 501 | }); 502 | ``` 503 | 504 | More generally, the daisy-chaining context keeps all parameters from the previous call around. This means in a situation where similar tracking calls are necessary tracking is simplified: 505 | 506 | ```javascript 507 | visitor 508 | .event({ec: "Mail Server", ea: "New Team Member Notification sent"}) 509 | .event({ea: "Invitation sent"}) 510 | .send(); 511 | ``` 512 | 513 | In this example the event category ("Mail Server") is not repeated in the second tracking call. 514 | 515 | 516 | 517 | 518 | # Setting persistent parameters 519 | 520 | Some parameters should be in every tracking call, such as a user ID or custom dimensions that never or hardly change. For such situations a `#set(key, value)` method is available 521 | 522 | ```javascript 523 | visitor.set("uid", "123456789"); 524 | ``` 525 | 526 | The uid parameter will be part of every tracking request of that visitor from now on. 527 | 528 | For custom dimensions, you will not pass `dimension#` rather `cd#`: 529 | 530 | ```javascript 531 | visitor.set("cd[1-20]", "123456789"); // [1-20] will be the dimension number 532 | ``` 533 | 534 | # Filter application tracking data 535 | 536 | Set a persistent parameter for [`Data Source`](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#ds) to `app` in order to mark tracking data as `Application`. 537 | 538 | ```javascript 539 | visitor.set("ds", "app"); // Allows filtering by the 'Application?' field in GA 540 | ``` 541 | 542 | Then create a new view in Google Analytics of type 'Application'. You will then need to filter the data for that view by creating a new filter that either includes or excludes `Application? Yes` (depending on if you want to show(includes) or hide(excludes) application analytics in a given view). 543 | 544 | ![Google Analytics Setup](https://i.imgur.com/ZDZ3ZPO.png) 545 | 546 | # Session-based identification 547 | 548 | In order to make session-based apps easier to work with, `universal-analytics` also provides a middleware that works in an Expressjs-style fashion. It will try to detect a client ID based on the `_ga` cookie used by the analytics.js client-side tracking. Additionally it will store the detected client ID in the current session to recognize the visitor later. 549 | 550 | ```javascript 551 | var ua = require("universal-analytics"); 552 | var express = require("express"); 553 | 554 | var app = express() 555 | 556 | express.use(ua.middleware("UA-XXXX-Y", {cookieName: '_ga'})); 557 | ``` 558 | 559 | The middleware will attach the `universal analytics` visitor instance to every request with the default name of `req.visitor`. The name of the instance on the `req` object can be overridden to avoid name conflicts by passing in `instanceName` on the `options` object: 560 | ```javascript 561 | express.use(ua.middleware("UA-XXXX-Y", {instanceName: 'uaVisitor'})); 562 | ``` 563 | 564 | Additionally, the module also exposes a `createFromSession` method to create a visitor instance simply based on a session, which is helpful when working with Socket.io, etc. where the middleware is not used. 565 | 566 | ```javascript 567 | var visitor = ua.createFromSession(socket.handshake.session); 568 | ``` 569 | 570 | # Debug mode 571 | 572 | `universal-analytics` is using the [`debug`](https://www.npmjs.com/package/debug) library. It can be instructed to output information during tracking by setting the `DEBUG` environment variable: 573 | 574 | ``` 575 | DEBUG=universal-analytics 576 | ``` 577 | 578 | 579 | # Request Options 580 | 581 | Due to the removal of the `request` package, request options are no longer available as of 0.5. 582 | 583 | 584 | # Shortcuts 585 | 586 | The tracking methods have shortcuts: 587 | 588 | * `Visitor#pv` as an alias for `Visitor#pageview` 589 | * `Visitor#e` as an alias for `Visitor#event` 590 | * `Visitor#t` as an alias for `Visitor#transaction` 591 | * `Visitor#i` as an alias for `Visitor#item` 592 | 593 | 594 | # Tests 595 | 596 | The tests are written with [mocha](https://github.com/visionmedia/mocha) using [should](https://github.com/visionmedia/should.js) and [Sinon.JS](https://github.com/cjohansen/Sinon.JS). 597 | 598 | Run them by executing the following commands in the `universal-analytics` directory: 599 | 600 | ``` 601 | $ npm install 602 | $ make test 603 | ``` 604 | 605 | # License 606 | 607 | (The MIT License) 608 | 609 | Copyright (c) 2017 Peaks & Pies GmbH <hello@peaksandpies.com> 610 | 611 | Permission is hereby granted, free of charge, to any person obtaining 612 | a copy of this software and associated documentation files (the 613 | 'Software'), to deal in the Software without restriction, including 614 | without limitation the rights to use, copy, modify, merge, publish, 615 | distribute, sublicense, and/or sell copies of the Software, and to 616 | permit persons to whom the Software is furnished to do so, subject to 617 | the following conditions: 618 | 619 | The above copyright notice and this permission notice shall be 620 | included in all copies or substantial portions of the Software. 621 | 622 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 623 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 624 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 625 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 626 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 627 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 628 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/index.js'); 3 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | protocolVersion: "1", 3 | hostname: "https://www.google-analytics.com", 4 | path: "/collect", 5 | batchPath: "/batch", 6 | batching: true, 7 | batchSize: 10, 8 | acceptedParameters: [ 9 | 10 | // General 11 | "v", "tid", "aip", "ds", "qt", "z", 12 | 13 | // User 14 | "cid", "uid", 15 | 16 | // Session 17 | "sc", "uip", "ua", "geoid", 18 | 19 | // Traffic Sources 20 | "dr", "cn", "cs", "cm", "ck", "cc", "ci", "gclid", "dclid", 21 | 22 | // System Info 23 | "sr", "vp", "de", "sd", "ul", "je", "fl", 24 | 25 | // Hit 26 | "t", "ni", 27 | 28 | // Content Information 29 | "dl", "dh", "dp", "dt", "cd", "linkid", 30 | 31 | // App Tracking 32 | "an", "aid", "av", "aiid", 33 | 34 | // Event Tracking 35 | "ec", "ea", "el", "ev", 36 | 37 | // E-commerce (transaction data: simple and enhanced) 38 | "ti", "ta", "tr", "ts", "tt", 39 | 40 | // E-commerce (item data: simple) 41 | "in", "ip", "iq", "ic", "iv", 42 | 43 | // E-commerce (currency: simple and enhanced) 44 | "cu", 45 | 46 | // Enhanced E-Commerce (see also: regex below) 47 | "pa", "tcc", "pal", "cos", "col", "promoa", 48 | 49 | // Social Interactions 50 | "sn", "sa", "st", 51 | 52 | // Timing 53 | "utc", "utv", "utt", "utl", "plt", "dns", "pdt", "rrt", "tcp", "srt", "dit", "clt", 54 | 55 | // Exceptions 56 | "exd", "exf", 57 | 58 | // Content Experiments 59 | "xid", "xvar"], 60 | 61 | acceptedParametersRegex: [ 62 | /^cm[0-9]+$/, 63 | /^cd[0-9]+$/, 64 | /^cg(10|[0-9])$/, 65 | 66 | /pr[0-9]{1,3}id/, 67 | /pr[0-9]{1,3}nm/, 68 | /pr[0-9]{1,3}br/, 69 | /pr[0-9]{1,3}ca/, 70 | /pr[0-9]{1,3}va/, 71 | /pr[0-9]{1,3}pr/, 72 | /pr[0-9]{1,3}qt/, 73 | /pr[0-9]{1,3}cc/, 74 | /pr[0-9]{1,3}ps/, 75 | /pr[0-9]{1,3}cd[0-9]{1,3}/, 76 | /pr[0-9]{1,3}cm[0-9]{1,3}/, 77 | 78 | /il[0-9]{1,3}nm/, 79 | /il[0-9]{1,3}pi[0-9]{1,3}id/, 80 | /il[0-9]{1,3}pi[0-9]{1,3}nm/, 81 | /il[0-9]{1,3}pi[0-9]{1,3}br/, 82 | /il[0-9]{1,3}pi[0-9]{1,3}ca/, 83 | /il[0-9]{1,3}pi[0-9]{1,3}va/, 84 | /il[0-9]{1,3}pi[0-9]{1,3}ps/, 85 | /il[0-9]{1,3}pi[0-9]{1,3}pr/, 86 | /il[0-9]{1,3}pi[0-9]{1,3}cd[0-9]{1,3}/, 87 | /il[0-9]{1,3}pi[0-9]{1,3}cm[0-9]{1,3}/, 88 | 89 | /promo[0-9]{1,3}id/, 90 | /promo[0-9]{1,3}nm/, 91 | /promo[0-9]{1,3}cr/, 92 | /promo[0-9]{1,3}ps/ 93 | ], 94 | parametersMap: { 95 | "protocolVersion": "v", 96 | "trackingId": "tid", 97 | "webPropertyId": "tid", 98 | "anonymizeIp": "aip", 99 | "dataSource": "ds", 100 | "queueTime": "qt", 101 | "cacheBuster": "z", 102 | "clientId": "cid", 103 | "userId": "uid", 104 | "sessionControl": "sc", 105 | "ipOverride": "uip", 106 | "userAgentOverride": "ua", 107 | "documentReferrer": "dr", 108 | "campaignName": "cn", 109 | "campaignSource": "cs", 110 | "campaignMedium": "cm", 111 | "campaignKeyword": "ck", 112 | "campaignContent": "cc", 113 | "campaignId": "ci", 114 | "googleAdwordsId": "gclid", 115 | "googleDisplayAdsId": "dclid", 116 | "screenResolution": "sr", 117 | "viewportSize": "vp", 118 | "documentEncoding": "de", 119 | "screenColors": "sd", 120 | "userLanguage": "ul", 121 | "javaEnabled": "je", 122 | "flashVersion": "fl", 123 | "hitType": "t", 124 | "non-interactionHit": "ni", 125 | "documentLocationUrl": "dl", 126 | "documentHostName": "dh", 127 | "documentPath": "dp", 128 | "documentTitle": "dt", 129 | "screenName": "cd", 130 | "linkId": "linkid", 131 | "applicationName": "an", 132 | "applicationId": "aid", 133 | "applicationVersion": "av", 134 | "applicationInstallerId": "aiid", 135 | "eventCategory": "ec", 136 | "eventAction": "ea", 137 | "eventLabel": "el", 138 | "eventValue": "ev", 139 | "transactionId": "ti", 140 | "transactionAffiliation": "ta", 141 | "transactionRevenue": "tr", 142 | "transactionShipping": "ts", 143 | "transactionTax": "tt", 144 | "itemName": "in", 145 | "itemPrice": "ip", 146 | "itemQuantity": "iq", 147 | "itemCode": "ic", 148 | "itemCategory": "iv", 149 | "currencyCode": "cu", 150 | "socialNetwork": "sn", 151 | "socialAction": "sa", 152 | "socialActionTarget": "st", 153 | "userTimingCategory": "utc", 154 | "userTimingVariableName": "utv", 155 | "userTimingTime": "utt", 156 | "userTimingLabel": "utl", 157 | "pageLoadTime": "plt", 158 | "dnsTime": "dns", 159 | "pageDownloadTime": "pdt", 160 | "redirectResponseTime": "rrt", 161 | "tcpConnectTime": "tcp", 162 | "serverResponseTime": "srt", 163 | "domInteractiveTime": "dit", 164 | "contentLoadTime": "clt", 165 | "exceptionDescription": "exd", 166 | "isExceptionFatal": "exf", 167 | "isExceptionFatal?": "exf", 168 | "experimentId": "xid", 169 | "experimentVariant": "xvar" 170 | } 171 | }; 172 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | var uuid = require("uuid"); 3 | var querystring = require("querystring"); 4 | var url = require("url"); 5 | 6 | var utils = require("./utils"); 7 | var config = require("./config"); 8 | var request = require('./request'); 9 | 10 | var debug = require("debug")("universal-analytics"); 11 | 12 | 13 | module.exports = init; 14 | 15 | 16 | function init (tid, cid, options) { 17 | return new Visitor(tid, cid, options); 18 | } 19 | 20 | var Visitor = module.exports.Visitor = function (tid, cid, options, context, persistentParams) { 21 | 22 | if (typeof tid === 'object') { 23 | options = tid; 24 | tid = cid = null; 25 | } else if (typeof cid === 'object') { 26 | options = cid; 27 | cid = null; 28 | } 29 | 30 | this._queue = []; 31 | 32 | this.options = options || {}; 33 | 34 | if(this.options.hostname) { 35 | config.hostname = this.options.hostname; 36 | } 37 | if(this.options.path) { 38 | config.path = this.options.path; 39 | } 40 | 41 | if (this.options.http) { 42 | var parsedHostname = url.parse(config.hostname); 43 | config.hostname = 'http://' + parsedHostname.host; 44 | } 45 | 46 | if(this.options.enableBatching !== undefined) { 47 | config.batching = options.enableBatching; 48 | } 49 | 50 | if(this.options.batchSize) { 51 | config.batchSize = this.options.batchSize; 52 | } 53 | 54 | this._context = context || {}; 55 | this._persistentParams = persistentParams || {}; 56 | 57 | this.tid = tid || this.options.tid; 58 | this.cid = this._determineCid(cid, this.options.cid, (this.options.strictCidFormat !== false)); 59 | if(this.options.uid) { 60 | this.uid = this.options.uid; 61 | } 62 | } 63 | 64 | 65 | 66 | 67 | module.exports.middleware = function (tid, options) { 68 | 69 | this.tid = tid; 70 | this.options = options || {}; 71 | 72 | var cookieName = this.options.cookieName || "_ga"; 73 | var instanceName = this.options.instanceName || "visitor"; 74 | delete this.options.instanceName; 75 | 76 | return function (req, res, next) { 77 | 78 | req[instanceName] = module.exports.createFromSession(req.session); 79 | 80 | if (req[instanceName]) return next(); 81 | 82 | var cid; 83 | if (req.cookies && req.cookies[cookieName]) { 84 | var gaSplit = req.cookies[cookieName].split('.'); 85 | cid = gaSplit[2] + "." + gaSplit[3]; 86 | } 87 | 88 | req[instanceName] = init(tid, cid, options); 89 | 90 | if (req.session) { 91 | req.session.cid = req[instanceName].cid; 92 | } 93 | 94 | next(); 95 | } 96 | } 97 | 98 | 99 | 100 | module.exports.createFromSession = function (session) { 101 | if (session && session.cid) { 102 | return init(this.tid, session.cid, this.options); 103 | } 104 | } 105 | 106 | 107 | 108 | Visitor.prototype = { 109 | 110 | debug: function (d) { 111 | debug.enabled = arguments.length === 0 ? true : d; 112 | debug("visitor.debug() is deprecated: set DEBUG=universal-analytics to enable logging") 113 | return this; 114 | }, 115 | 116 | 117 | reset: function () { 118 | this._context = null; 119 | return this; 120 | }, 121 | 122 | set: function (key, value) { 123 | this._persistentParams = this._persistentParams || {}; 124 | this._persistentParams[key] = value; 125 | }, 126 | 127 | pageview: function (path, hostname, title, params, fn) { 128 | 129 | if (typeof path === 'object' && path != null) { 130 | params = path; 131 | if (typeof hostname === 'function') { 132 | fn = hostname 133 | } 134 | path = hostname = title = null; 135 | } else if (typeof hostname === 'function') { 136 | fn = hostname 137 | hostname = title = null; 138 | } else if (typeof title === 'function') { 139 | fn = title; 140 | title = null; 141 | } else if (typeof params === 'function') { 142 | fn = params; 143 | params = null; 144 | } 145 | 146 | params = this._translateParams(params); 147 | 148 | params = Object.assign({}, this._persistentParams || {}, params); 149 | 150 | params.dp = path || params.dp || this._context.dp; 151 | params.dh = hostname || params.dh || this._context.dh; 152 | params.dt = title || params.dt || this._context.dt; 153 | 154 | this._tidyParameters(params); 155 | 156 | if (!params.dp && !params.dl) { 157 | return this._handleError("Please provide either a page path (dp) or a document location (dl)", fn); 158 | } 159 | 160 | return this._withContext(params)._enqueue("pageview", params, fn); 161 | }, 162 | 163 | 164 | screenview: function (screenName, appName, appVersion, appId, appInstallerId, params, fn) { 165 | 166 | if (typeof screenName === 'object' && screenName != null) { 167 | params = screenName; 168 | if (typeof appName === 'function') { 169 | fn = appName 170 | } 171 | screenName = appName = appVersion = appId = appInstallerId = null; 172 | } else if (typeof appName === 'function') { 173 | fn = appName 174 | appName = appVersion = appId = appInstallerId = null; 175 | } else if (typeof appVersion === 'function') { 176 | fn = appVersion; 177 | appVersion = appId = appInstallerId = null; 178 | } else if (typeof appId === 'function') { 179 | fn = appId; 180 | appId = appInstallerId = null; 181 | } else if (typeof appInstallerId === 'function') { 182 | fn = appInstallerId; 183 | appInstallerId = null; 184 | } else if (typeof params === 'function') { 185 | fn = params; 186 | params = null; 187 | } 188 | 189 | params = this._translateParams(params); 190 | 191 | params = Object.assign({}, this._persistentParams || {}, params); 192 | 193 | params.cd = screenName || params.cd || this._context.cd; 194 | params.an = appName || params.an || this._context.an; 195 | params.av = appVersion || params.av || this._context.av; 196 | params.aid = appId || params.aid || this._context.aid; 197 | params.aiid = appInstallerId || params.aiid || this._context.aiid; 198 | 199 | this._tidyParameters(params); 200 | 201 | if (!params.cd || !params.an) { 202 | return this._handleError("Please provide at least a screen name (cd) and an app name (an)", fn); 203 | } 204 | 205 | return this._withContext(params)._enqueue("screenview", params, fn); 206 | }, 207 | 208 | 209 | event: function (category, action, label, value, params, fn) { 210 | 211 | if (typeof category === 'object' && category != null) { 212 | params = category; 213 | if (typeof action === 'function') { 214 | fn = action 215 | } 216 | category = action = label = value = null; 217 | } else if (typeof label === 'function') { 218 | fn = label; 219 | label = value = null; 220 | } else if (typeof value === 'function') { 221 | fn = value; 222 | value = null; 223 | } else if (typeof params === 'function') { 224 | fn = params; 225 | params = null; 226 | } 227 | 228 | params = this._translateParams(params); 229 | 230 | params = Object.assign({}, this._persistentParams || {}, params); 231 | 232 | params.ec = category || params.ec || this._context.ec; 233 | params.ea = action || params.ea || this._context.ea; 234 | params.el = label || params.el || this._context.el; 235 | params.ev = value || params.ev || this._context.ev; 236 | params.p = params.p || params.dp || this._context.p || this._context.dp; 237 | 238 | delete params.dp; 239 | this._tidyParameters(params); 240 | 241 | if (!params.ec || !params.ea) { 242 | return this._handleError("Please provide at least an event category (ec) and an event action (ea)", fn); 243 | } 244 | 245 | return this._withContext(params)._enqueue("event", params, fn); 246 | }, 247 | 248 | 249 | transaction: function (transaction, revenue, shipping, tax, affiliation, params, fn) { 250 | if (typeof transaction === 'object') { 251 | params = transaction; 252 | if (typeof revenue === 'function') { 253 | fn = revenue 254 | } 255 | transaction = revenue = shipping = tax = affiliation = null; 256 | } else if (typeof revenue === 'function') { 257 | fn = revenue; 258 | revenue = shipping = tax = affiliation = null; 259 | } else if (typeof shipping === 'function') { 260 | fn = shipping; 261 | shipping = tax = affiliation = null; 262 | } else if (typeof tax === 'function') { 263 | fn = tax; 264 | tax = affiliation = null; 265 | } else if (typeof affiliation === 'function') { 266 | fn = affiliation; 267 | affiliation = null; 268 | } else if (typeof params === 'function') { 269 | fn = params; 270 | params = null; 271 | } 272 | 273 | params = this._translateParams(params); 274 | 275 | params = Object.assign({}, this._persistentParams || {}, params); 276 | 277 | params.ti = transaction || params.ti || this._context.ti; 278 | params.tr = revenue || params.tr || this._context.tr; 279 | params.ts = shipping || params.ts || this._context.ts; 280 | params.tt = tax || params.tt || this._context.tt; 281 | params.ta = affiliation || params.ta || this._context.ta; 282 | params.p = params.p || this._context.p || this._context.dp; 283 | 284 | this._tidyParameters(params); 285 | 286 | if (!params.ti) { 287 | return this._handleError("Please provide at least a transaction ID (ti)", fn); 288 | } 289 | 290 | return this._withContext(params)._enqueue("transaction", params, fn); 291 | }, 292 | 293 | 294 | item: function (price, quantity, sku, name, variation, params, fn) { 295 | if (typeof price === 'object') { 296 | params = price; 297 | if (typeof quantity === 'function') { 298 | fn = quantity 299 | } 300 | price = quantity = sku = name = variation = null; 301 | } else if (typeof quantity === 'function') { 302 | fn = quantity; 303 | quantity = sku = name = variation = null; 304 | } else if (typeof sku === 'function') { 305 | fn = sku; 306 | sku = name = variation = null; 307 | } else if (typeof name === 'function') { 308 | fn = name; 309 | name = variation = null; 310 | } else if (typeof variation === 'function') { 311 | fn = variation; 312 | variation = null; 313 | } else if (typeof params === 'function') { 314 | fn = params; 315 | params = null; 316 | } 317 | 318 | params = this._translateParams(params); 319 | 320 | params = Object.assign({}, this._persistentParams || {}, params); 321 | 322 | params.ip = price || params.ip || this._context.ip; 323 | params.iq = quantity || params.iq || this._context.iq; 324 | params.ic = sku || params.ic || this._context.ic; 325 | params.in = name || params.in || this._context.in; 326 | params.iv = variation || params.iv || this._context.iv; 327 | params.p = params.p || this._context.p || this._context.dp; 328 | params.ti = params.ti || this._context.ti; 329 | 330 | this._tidyParameters(params); 331 | 332 | if (!params.ti) { 333 | return this._handleError("Please provide at least an item transaction ID (ti)", fn); 334 | } 335 | 336 | return this._withContext(params)._enqueue("item", params, fn); 337 | 338 | }, 339 | 340 | exception: function (description, fatal, params, fn) { 341 | 342 | if (typeof description === 'object') { 343 | params = description; 344 | if (typeof fatal === 'function') { 345 | fn = fatal; 346 | } 347 | description = fatal = null; 348 | } else if (typeof fatal === 'function') { 349 | fn = fatal; 350 | fatal = 0; 351 | } else if (typeof params === 'function') { 352 | fn = params; 353 | params = null; 354 | } 355 | 356 | params = this._translateParams(params); 357 | 358 | params = Object.assign({}, this._persistentParams || {}, params); 359 | 360 | params.exd = description || params.exd || this._context.exd; 361 | params.exf = +!!(fatal || params.exf || this._context.exf); 362 | 363 | if (params.exf === 0) { 364 | delete params.exf; 365 | } 366 | 367 | this._tidyParameters(params); 368 | 369 | return this._withContext(params)._enqueue("exception", params, fn); 370 | }, 371 | 372 | timing: function (category, variable, time, label, params, fn) { 373 | 374 | if (typeof category === 'object') { 375 | params = category; 376 | if (typeof variable === 'function') { 377 | fn = variable; 378 | } 379 | category = variable = time = label = null; 380 | } else if (typeof variable === 'function') { 381 | fn = variable; 382 | variable = time = label = null; 383 | } else if (typeof time === 'function') { 384 | fn = time; 385 | time = label = null; 386 | } else if (typeof label === 'function') { 387 | fn = label; 388 | label = null; 389 | } else if (typeof params === 'function') { 390 | fn = params; 391 | params = null; 392 | } 393 | 394 | params = this._translateParams(params); 395 | 396 | params = Object.assign({}, this._persistentParams || {}, params); 397 | 398 | params.utc = category || params.utc || this._context.utc; 399 | params.utv = variable || params.utv || this._context.utv; 400 | params.utt = time || params.utt || this._context.utt; 401 | params.utl = label || params.utl || this._context.utl; 402 | 403 | this._tidyParameters(params); 404 | 405 | return this._withContext(params)._enqueue("timing", params, fn); 406 | }, 407 | 408 | 409 | send: function (fn) { 410 | var self = this; 411 | var count = 1; 412 | var fn = fn || function () {}; 413 | debug("Sending %d tracking call(s)", self._queue.length); 414 | 415 | var getBody = function(params) { 416 | return params.map(function(x) { return querystring.stringify(x); }).join("\n"); 417 | } 418 | 419 | var onFinish = function (err) { 420 | debug("Finished sending tracking calls") 421 | fn.call(self, err || null, count - 1); 422 | } 423 | 424 | var iterator = function () { 425 | if (!self._queue.length) { 426 | return onFinish(null); 427 | } 428 | var params = []; 429 | 430 | if(config.batching) { 431 | params = self._queue.splice(0, Math.min(self._queue.length, config.batchSize)); 432 | } else { 433 | params.push(self._queue.shift()); 434 | } 435 | 436 | var useBatchPath = params.length > 1; 437 | 438 | var path = config.hostname + (useBatchPath ? config.batchPath : config.path); 439 | 440 | debug("%d: %o", count++, params); 441 | 442 | var body = getBody(params); 443 | 444 | request.post(path, body, self.options.headers, nextIteration); 445 | } 446 | 447 | function nextIteration(err) { 448 | if (err) return onFinish(err); 449 | iterator(); 450 | } 451 | 452 | iterator(); 453 | 454 | }, 455 | 456 | _enqueue: function (type, params, fn) { 457 | 458 | if (typeof params === 'function') { 459 | fn = params; 460 | params = {}; 461 | } 462 | 463 | params = this._translateParams(params) || {}; 464 | 465 | Object.assign(params, { 466 | v: config.protocolVersion, 467 | tid: this.tid, 468 | cid: this.cid, 469 | t: type 470 | }); 471 | if(this.uid) { 472 | params.uid = this.uid; 473 | } 474 | 475 | this._queue.push(params); 476 | 477 | if (debug.enabled) { 478 | this._checkParameters(params); 479 | } 480 | 481 | debug("Enqueued %s (%o)", type, params); 482 | 483 | if (fn) { 484 | this.send(fn); 485 | } 486 | 487 | return this; 488 | }, 489 | 490 | 491 | _handleError: function (message, fn) { 492 | debug("Error: %s", message) 493 | fn && fn.call(this, new Error(message)) 494 | return this; 495 | }, 496 | 497 | 498 | 499 | _determineCid: function () { 500 | var args = Array.prototype.splice.call(arguments, 0); 501 | var id; 502 | var lastItem = args.length-1; 503 | var strict = args[lastItem]; 504 | if (strict) { 505 | for (var i = 0; i < lastItem; i++) { 506 | id = utils.ensureValidCid(args[i]); 507 | if (id !== false) return id; 508 | if (id != null) debug("Warning! Invalid UUID format '%s'", args[i]); 509 | } 510 | } else { 511 | for (var i = 0; i < lastItem; i++) { 512 | if (args[i]) return args[i]; 513 | } 514 | } 515 | return uuid.v4(); 516 | }, 517 | 518 | 519 | _checkParameters: function (params) { 520 | for (var param in params) { 521 | if (config.acceptedParameters.indexOf(param) !== -1 || config.acceptedParametersRegex.filter(function (r) { 522 | return r.test(param); 523 | }).length) { 524 | continue; 525 | } 526 | debug("Warning! Unsupported tracking parameter %s (%s)", param, params[param]); 527 | } 528 | }, 529 | 530 | _translateParams: function (params) { 531 | var translated = {}; 532 | for (var key in params) { 533 | if (config.parametersMap.hasOwnProperty(key)) { 534 | translated[config.parametersMap[key]] = params[key]; 535 | } else { 536 | translated[key] = params[key]; 537 | } 538 | } 539 | return translated; 540 | }, 541 | 542 | _tidyParameters: function (params) { 543 | for (var param in params) { 544 | if (params[param] === null || params[param] === undefined) { 545 | delete params[param]; 546 | } 547 | } 548 | return params; 549 | }, 550 | 551 | _withContext: function (context) { 552 | var visitor = new Visitor(this.tid, this.cid, this.options, context, this._persistentParams); 553 | visitor._queue = this._queue; 554 | return visitor; 555 | } 556 | 557 | 558 | } 559 | 560 | Visitor.prototype.pv = Visitor.prototype.pageview 561 | Visitor.prototype.e = Visitor.prototype.event 562 | Visitor.prototype.t = Visitor.prototype.transaction 563 | Visitor.prototype.i = Visitor.prototype.item 564 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | `use strict` 2 | 3 | const http = require('http'); 4 | const https = require('https'); 5 | const url = require('url'); 6 | const debug = require("debug")("universal-analytics"); 7 | 8 | function getProtocol(path) { 9 | return url.parse(path).protocol === "http:" ? http : https; 10 | } 11 | 12 | /** 13 | * Send a post request 14 | * @param path is the url endpoint 15 | * @param headers of the request 16 | * @param callback contains (error, body, status, headers) 17 | * @param data a JSON Object or a string 18 | */ 19 | function post(path, data, headers, callback) { 20 | request(path, "POST", data, headers, callback); 21 | } 22 | 23 | /** 24 | * Send a custom request 25 | * @param path is the url endpoint 26 | * @param headers of the request 27 | * @param callback contains (error, statusCode, data) 28 | * @param data a JSON Object or a string 29 | * @param method is the protocol used like POST GET DELETE PUT etc... 30 | */ 31 | function request(path, method, body, headers = {}, callback) { 32 | const { hostname, port, pathname } = url.parse(path); 33 | const options = { 34 | hostname, 35 | port, 36 | path: pathname, 37 | method, 38 | headers 39 | }; 40 | 41 | const req = getProtocol(path).request(options, function (response) { 42 | handleResponse(response, callback); 43 | }); 44 | 45 | req.on('error', function (error) { 46 | callback(error); 47 | debug('Request error', error); 48 | }); 49 | 50 | req.write(body); 51 | 52 | req.end(); 53 | } 54 | 55 | function handleResponse(response, callback) { 56 | let body = ''; 57 | const { headers, statusCode } = response 58 | const hasError = statusCode >= 300; 59 | 60 | response.setEncoding('utf8'); 61 | 62 | response.on('data', function (data) { 63 | body += data; 64 | }); 65 | 66 | response.on('end', function () { 67 | callback(hasError ? body : null, hasError ? null : body, statusCode, headers); 68 | }); 69 | } 70 | 71 | module.exports = { 72 | post 73 | }; 74 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports.isUuid = function (uuid) { 3 | if (!uuid) return false; 4 | uuid = uuid.toString().toLowerCase(); 5 | return /[0-9a-f]{8}\-?[0-9a-f]{4}\-?4[0-9a-f]{3}\-?[89ab][0-9a-f]{3}\-?[0-9a-f]{12}/.test(uuid) 6 | } 7 | 8 | 9 | module.exports.isCookieCid = function (cid) { 10 | return /^[0-9]+\.[0-9]+$/.test(cid) 11 | } 12 | 13 | 14 | module.exports.ensureValidCid = function (uuid) { 15 | if (!this.isUuid(uuid)) { 16 | if (!this.isCookieCid(uuid)) { 17 | return false; 18 | } 19 | return uuid; 20 | } 21 | 22 | uuid = uuid.replace(/\-/g, ""); 23 | return "" + 24 | uuid.substring(0, 8) + "-" + 25 | uuid.substring(8, 12) + "-" + 26 | uuid.substring(12, 16) + "-" + 27 | uuid.substring(16, 20) + "-" + 28 | uuid.substring(20); 29 | } 30 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-analytics", 3 | "version": "0.5.3", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "debug": { 8 | "version": "4.3.1", 9 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 10 | "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", 11 | "requires": { 12 | "ms": "2.1.2" 13 | } 14 | }, 15 | "mocha": { 16 | "version": "3.5.0", 17 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.0.tgz", 18 | "integrity": "sha1-EyhWfScX+ZcDD4AGI0vOm4zXJGU=", 19 | "dev": true, 20 | "requires": { 21 | "browser-stdout": "1.3.0", 22 | "commander": "2.9.0", 23 | "debug": "2.6.8", 24 | "diff": "3.2.0", 25 | "escape-string-regexp": "1.0.5", 26 | "glob": "7.1.1", 27 | "growl": "1.9.2", 28 | "json3": "3.3.2", 29 | "lodash.create": "3.1.1", 30 | "mkdirp": "0.5.1", 31 | "supports-color": "3.1.2" 32 | }, 33 | "dependencies": { 34 | "browser-stdout": { 35 | "version": "1.3.0", 36 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", 37 | "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", 38 | "dev": true 39 | }, 40 | "commander": { 41 | "version": "2.9.0", 42 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", 43 | "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", 44 | "dev": true, 45 | "requires": { 46 | "graceful-readlink": ">= 1.0.0" 47 | }, 48 | "dependencies": { 49 | "graceful-readlink": { 50 | "version": "1.0.1", 51 | "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", 52 | "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", 53 | "dev": true 54 | } 55 | } 56 | }, 57 | "debug": { 58 | "version": "2.6.8", 59 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", 60 | "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", 61 | "dev": true, 62 | "requires": { 63 | "ms": "2.0.0" 64 | }, 65 | "dependencies": { 66 | "ms": { 67 | "version": "2.0.0", 68 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 69 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 70 | "dev": true 71 | } 72 | } 73 | }, 74 | "diff": { 75 | "version": "3.2.0", 76 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", 77 | "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", 78 | "dev": true 79 | }, 80 | "escape-string-regexp": { 81 | "version": "1.0.5", 82 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 83 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 84 | "dev": true 85 | }, 86 | "glob": { 87 | "version": "7.1.1", 88 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", 89 | "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", 90 | "dev": true, 91 | "requires": { 92 | "fs.realpath": "^1.0.0", 93 | "inflight": "^1.0.4", 94 | "inherits": "2", 95 | "minimatch": "^3.0.2", 96 | "once": "^1.3.0", 97 | "path-is-absolute": "^1.0.0" 98 | }, 99 | "dependencies": { 100 | "fs.realpath": { 101 | "version": "1.0.0", 102 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 103 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 104 | "dev": true 105 | }, 106 | "inflight": { 107 | "version": "1.0.6", 108 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 109 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 110 | "dev": true, 111 | "requires": { 112 | "once": "^1.3.0", 113 | "wrappy": "1" 114 | }, 115 | "dependencies": { 116 | "wrappy": { 117 | "version": "1.0.2", 118 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 119 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 120 | "dev": true 121 | } 122 | } 123 | }, 124 | "inherits": { 125 | "version": "2.0.3", 126 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 127 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 128 | "dev": true 129 | }, 130 | "minimatch": { 131 | "version": "3.0.4", 132 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 133 | "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", 134 | "dev": true, 135 | "requires": { 136 | "brace-expansion": "^1.1.7" 137 | }, 138 | "dependencies": { 139 | "brace-expansion": { 140 | "version": "1.1.8", 141 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", 142 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", 143 | "dev": true, 144 | "requires": { 145 | "balanced-match": "^1.0.0", 146 | "concat-map": "0.0.1" 147 | }, 148 | "dependencies": { 149 | "balanced-match": { 150 | "version": "1.0.0", 151 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 152 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 153 | "dev": true 154 | }, 155 | "concat-map": { 156 | "version": "0.0.1", 157 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 158 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 159 | "dev": true 160 | } 161 | } 162 | } 163 | } 164 | }, 165 | "once": { 166 | "version": "1.4.0", 167 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 168 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 169 | "dev": true, 170 | "requires": { 171 | "wrappy": "1" 172 | }, 173 | "dependencies": { 174 | "wrappy": { 175 | "version": "1.0.2", 176 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 177 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 178 | "dev": true 179 | } 180 | } 181 | }, 182 | "path-is-absolute": { 183 | "version": "1.0.1", 184 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 185 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 186 | "dev": true 187 | } 188 | } 189 | }, 190 | "growl": { 191 | "version": "1.9.2", 192 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", 193 | "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", 194 | "dev": true 195 | }, 196 | "json3": { 197 | "version": "3.3.2", 198 | "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", 199 | "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", 200 | "dev": true 201 | }, 202 | "lodash.create": { 203 | "version": "3.1.1", 204 | "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", 205 | "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", 206 | "dev": true, 207 | "requires": { 208 | "lodash._baseassign": "^3.0.0", 209 | "lodash._basecreate": "^3.0.0", 210 | "lodash._isiterateecall": "^3.0.0" 211 | }, 212 | "dependencies": { 213 | "lodash._baseassign": { 214 | "version": "3.2.0", 215 | "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", 216 | "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", 217 | "dev": true, 218 | "requires": { 219 | "lodash._basecopy": "^3.0.0", 220 | "lodash.keys": "^3.0.0" 221 | }, 222 | "dependencies": { 223 | "lodash._basecopy": { 224 | "version": "3.0.1", 225 | "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", 226 | "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", 227 | "dev": true 228 | }, 229 | "lodash.keys": { 230 | "version": "3.1.2", 231 | "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", 232 | "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", 233 | "dev": true, 234 | "requires": { 235 | "lodash._getnative": "^3.0.0", 236 | "lodash.isarguments": "^3.0.0", 237 | "lodash.isarray": "^3.0.0" 238 | }, 239 | "dependencies": { 240 | "lodash._getnative": { 241 | "version": "3.9.1", 242 | "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", 243 | "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", 244 | "dev": true 245 | }, 246 | "lodash.isarguments": { 247 | "version": "3.1.0", 248 | "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", 249 | "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", 250 | "dev": true 251 | }, 252 | "lodash.isarray": { 253 | "version": "3.0.4", 254 | "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", 255 | "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", 256 | "dev": true 257 | } 258 | } 259 | } 260 | } 261 | }, 262 | "lodash._basecreate": { 263 | "version": "3.0.3", 264 | "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", 265 | "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", 266 | "dev": true 267 | }, 268 | "lodash._isiterateecall": { 269 | "version": "3.0.9", 270 | "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", 271 | "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", 272 | "dev": true 273 | } 274 | } 275 | }, 276 | "mkdirp": { 277 | "version": "0.5.1", 278 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 279 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 280 | "dev": true, 281 | "requires": { 282 | "minimist": "0.0.8" 283 | }, 284 | "dependencies": { 285 | "minimist": { 286 | "version": "0.0.8", 287 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 288 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 289 | "dev": true 290 | } 291 | } 292 | }, 293 | "supports-color": { 294 | "version": "3.1.2", 295 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", 296 | "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", 297 | "dev": true, 298 | "requires": { 299 | "has-flag": "^1.0.0" 300 | }, 301 | "dependencies": { 302 | "has-flag": { 303 | "version": "1.0.0", 304 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", 305 | "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", 306 | "dev": true 307 | } 308 | } 309 | } 310 | } 311 | }, 312 | "ms": { 313 | "version": "2.1.2", 314 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 315 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 316 | }, 317 | "should": { 318 | "version": "11.2.1", 319 | "resolved": "https://registry.npmjs.org/should/-/should-11.2.1.tgz", 320 | "integrity": "sha1-kPVRRVUtAc/CAGZuToGKHJZw7aI=", 321 | "dev": true, 322 | "requires": { 323 | "should-equal": "^1.0.0", 324 | "should-format": "^3.0.2", 325 | "should-type": "^1.4.0", 326 | "should-type-adaptors": "^1.0.1", 327 | "should-util": "^1.0.0" 328 | }, 329 | "dependencies": { 330 | "should-equal": { 331 | "version": "1.0.1", 332 | "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-1.0.1.tgz", 333 | "integrity": "sha1-C26VFvJgGp+wuy3MNpr6HH4gCvc=", 334 | "dev": true, 335 | "requires": { 336 | "should-type": "^1.0.0" 337 | } 338 | } 339 | } 340 | }, 341 | "should-format": { 342 | "version": "3.0.3", 343 | "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", 344 | "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", 345 | "dev": true, 346 | "requires": { 347 | "should-type": "^1.3.0", 348 | "should-type-adaptors": "^1.0.1" 349 | } 350 | }, 351 | "should-type": { 352 | "version": "1.4.0", 353 | "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", 354 | "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=", 355 | "dev": true 356 | }, 357 | "should-type-adaptors": { 358 | "version": "1.1.0", 359 | "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", 360 | "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", 361 | "dev": true, 362 | "requires": { 363 | "should-type": "^1.3.0", 364 | "should-util": "^1.0.0" 365 | } 366 | }, 367 | "should-util": { 368 | "version": "1.0.1", 369 | "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", 370 | "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", 371 | "dev": true 372 | }, 373 | "sinon": { 374 | "version": "1.17.7", 375 | "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", 376 | "integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=", 377 | "dev": true, 378 | "requires": { 379 | "formatio": "1.1.1", 380 | "lolex": "1.3.2", 381 | "samsam": "1.1.2", 382 | "util": ">=0.10.3 <1" 383 | }, 384 | "dependencies": { 385 | "formatio": { 386 | "version": "1.1.1", 387 | "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", 388 | "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", 389 | "dev": true, 390 | "requires": { 391 | "samsam": "~1.1" 392 | } 393 | }, 394 | "lolex": { 395 | "version": "1.3.2", 396 | "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", 397 | "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=", 398 | "dev": true 399 | }, 400 | "samsam": { 401 | "version": "1.1.2", 402 | "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", 403 | "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=", 404 | "dev": true 405 | }, 406 | "util": { 407 | "version": "0.10.3", 408 | "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", 409 | "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", 410 | "dev": true, 411 | "requires": { 412 | "inherits": "2.0.1" 413 | }, 414 | "dependencies": { 415 | "inherits": { 416 | "version": "2.0.1", 417 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", 418 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", 419 | "dev": true 420 | } 421 | } 422 | } 423 | } 424 | }, 425 | "uuid": { 426 | "version": "8.3.2", 427 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 428 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" 429 | } 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-analytics", 3 | "version": "0.5.3", 4 | "description": "A node module for Google's Universal Analytics tracking", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:peaksandpies/universal-analytics.git" 12 | }, 13 | "keywords": [ 14 | "google", 15 | "analytics", 16 | "universal", 17 | "tracking" 18 | ], 19 | "dependencies": { 20 | "debug": "^4.3.1", 21 | "uuid": "^8.0.0" 22 | }, 23 | "devDependencies": { 24 | "mocha": "*", 25 | "should": "*", 26 | "sinon": "^1.17.7" 27 | }, 28 | "author": "Jörg Tillmann ", 29 | "license": "MIT", 30 | "engines": { 31 | "node": ">=12.18.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/_enqueue.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require("querystring"); 3 | var uuid = require("uuid"); 4 | var should = require("should"); 5 | var sinon = require("sinon"); 6 | var url = require("url"); 7 | 8 | var ua = require("../lib/index.js"); 9 | var utils = require("../lib/utils.js") 10 | var config = require("../lib/config.js") 11 | var request = require("../lib/request"); 12 | 13 | 14 | describe("ua", function () { 15 | 16 | 17 | describe("#_enqueue", function () { 18 | 19 | var send; 20 | 21 | beforeEach(function () { 22 | send = sinon.stub(ua.Visitor.prototype, "send").callsArg(0); 23 | }); 24 | 25 | afterEach(function () { 26 | send.restore() 27 | }); 28 | 29 | it("should accept arguments (type)", function () { 30 | var tid = "UA-XXXXX-XX"; 31 | var cid = uuid.v4(); 32 | var type = Math.random().toString() 33 | 34 | var visitor = ua(tid, cid)._enqueue(type); 35 | 36 | send.called.should.equal(false, "#send should not have been called without a callback"); 37 | 38 | visitor._queue.length.should.equal(1, "1 tracking call should have been enqueued"); 39 | 40 | visitor._queue[0].should.have.keys("v", "tid", "cid", "t") 41 | visitor._queue[0].tid.should.equal(tid) 42 | visitor._queue[0].cid.should.equal(cid) 43 | visitor._queue[0].t.should.equal(type) 44 | }); 45 | 46 | it("should accept arguments (type, fn)", function () { 47 | var tid = "UA-XXXXX-XX"; 48 | var cid = uuid.v4(); 49 | var type = Math.random().toString() 50 | var fn = sinon.spy() 51 | 52 | var visitor = ua(tid, cid)._enqueue(type, fn); 53 | 54 | send.calledOnce.should.equal(true, "#send should have been called once"); 55 | 56 | visitor._queue.length.should.equal(1, "1 tracking call should have been enqueued"); 57 | 58 | visitor._queue[0].should.have.keys("v", "tid", "cid", "t") 59 | visitor._queue[0].tid.should.equal(tid) 60 | visitor._queue[0].cid.should.equal(cid) 61 | visitor._queue[0].t.should.equal(type) 62 | 63 | fn.calledOnce.should.equal(true, "callback should have been called once") 64 | }); 65 | 66 | it("should accept arguments (type, params)", function () { 67 | var tid = "UA-XXXXX-XX"; 68 | var cid = uuid.v4(); 69 | var type = Math.random().toString() 70 | var params = {foo: Math.random().toString()} 71 | 72 | var visitor = ua(tid, cid)._enqueue(type, params); 73 | 74 | send.called.should.equal(false, "#send should not have been called without a callback"); 75 | 76 | visitor._queue.length.should.equal(1, "1 tracking call should have been enqueued"); 77 | 78 | visitor._queue[0].should.have.keys("v", "tid", "cid", "t", "foo") 79 | visitor._queue[0].tid.should.equal(tid) 80 | visitor._queue[0].cid.should.equal(cid) 81 | visitor._queue[0].foo.should.equal(params.foo); 82 | }); 83 | 84 | it("should add userId if present on the Visitor", function() { 85 | var tid = "UA-XXXXX-XX"; 86 | var cid = uuid.v4(); 87 | var type = "type"; 88 | var uid = "user1"; 89 | var params = {} 90 | 91 | var visitor = ua(tid, cid, { uid: uid})._enqueue(type, params); 92 | 93 | visitor._queue[0].uid.should.equal(uid); 94 | }); 95 | 96 | it("should accept arguments (type, params, fn)", function () { 97 | var tid = "UA-XXXXX-XX"; 98 | var cid = uuid.v4(); 99 | var type = Math.random().toString() 100 | var params = {foo: Math.random().toString()} 101 | var fn = sinon.spy() 102 | 103 | var visitor = ua(tid, cid)._enqueue(type, params, fn); 104 | 105 | send.calledOnce.should.equal(true, "#send should have been called once"); 106 | 107 | visitor._queue.length.should.equal(1, "1 tracking call should have been enqueued"); 108 | 109 | visitor._queue[0].should.have.keys("v", "tid", "cid", "t", "foo") 110 | visitor._queue[0].tid.should.equal(tid) 111 | visitor._queue[0].cid.should.equal(cid) 112 | visitor._queue[0].foo.should.equal(params.foo); 113 | 114 | fn.calledOnce.should.equal(true, "callback should have been called once") 115 | }); 116 | 117 | it("should continue adding to the queue", function () { 118 | var tid = "UA-XXXXX-XX"; 119 | var cid = uuid.v4(); 120 | var type = Math.random().toString() 121 | 122 | var visitor = ua(tid, cid) 123 | 124 | visitor._enqueue(type); 125 | visitor._enqueue(type); 126 | visitor._enqueue(type); 127 | visitor._enqueue(type); 128 | 129 | visitor._queue.length.should.equal(4, "4 tracking calls should have been enqueued"); 130 | }) 131 | 132 | }); 133 | 134 | }); 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /test/event.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require("querystring"); 3 | var uuid = require("uuid"); 4 | var should = require("should"); 5 | var sinon = require("sinon"); 6 | var url = require("url"); 7 | 8 | var ua = require("../lib/index.js"); 9 | var utils = require("../lib/utils.js") 10 | var config = require("../lib/config.js") 11 | var request = require("../lib/request"); 12 | 13 | 14 | describe("ua", function () { 15 | 16 | 17 | describe("#event", function () { 18 | var _enqueue; 19 | 20 | beforeEach(function () { 21 | _enqueue = sinon.stub(ua.Visitor.prototype, "_enqueue", function () { 22 | if (arguments.length === 3 && typeof arguments[2] === 'function') { 23 | arguments[2](); 24 | } 25 | return this; 26 | }); 27 | }); 28 | 29 | afterEach(function () { 30 | _enqueue.restore() 31 | }); 32 | 33 | 34 | it("should be available via the #e shortcut", function () { 35 | var visitor = ua() 36 | visitor.e.should.equal(visitor.event) 37 | }); 38 | 39 | 40 | it("should accept arguments (category, action)", function () { 41 | var category = Math.random().toString(); 42 | var action = Math.random().toString(); 43 | 44 | var visitor = ua() 45 | 46 | var result = visitor.event(category, action); 47 | 48 | visitor._context = result._context; 49 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 50 | 51 | result.should.be.instanceof(ua.Visitor); 52 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 53 | 54 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 55 | _enqueue.args[0][0].should.equal("event"); 56 | _enqueue.args[0][1].should.have.keys("ec", "ea") 57 | _enqueue.args[0][1].ec.should.equal(category); 58 | _enqueue.args[0][1].ea.should.equal(action); 59 | }); 60 | 61 | it("should accept arguments (category, action, fn)", function () { 62 | var category = Math.random().toString(); 63 | var action = Math.random().toString(); 64 | var fn = sinon.spy() 65 | 66 | ua().event(category, action, fn); 67 | 68 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 69 | _enqueue.args[0][0].should.equal("event"); 70 | _enqueue.args[0][1].should.have.keys("ec", "ea") 71 | _enqueue.args[0][1].ec.should.equal(category); 72 | _enqueue.args[0][1].ea.should.equal(action); 73 | 74 | fn.calledOnce.should.equal(true, "callback should have been called once") 75 | }); 76 | 77 | it("should accept arguments (category, action, label)", function () { 78 | var category = Math.random().toString(); 79 | var action = Math.random().toString(); 80 | var label = Math.random().toString(); 81 | 82 | ua().event(category, action, label); 83 | 84 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 85 | _enqueue.args[0][0].should.equal("event"); 86 | _enqueue.args[0][1].should.have.keys("ec", "ea", "el") 87 | _enqueue.args[0][1].ec.should.equal(category); 88 | _enqueue.args[0][1].ea.should.equal(action); 89 | _enqueue.args[0][1].el.should.equal(label); 90 | }); 91 | 92 | it("should accept arguments (category, action, label, fn)", function () { 93 | var category = Math.random().toString(); 94 | var action = Math.random().toString(); 95 | var label = Math.random().toString(); 96 | var fn = sinon.spy() 97 | 98 | ua().event(category, action, label, fn); 99 | 100 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 101 | _enqueue.args[0][0].should.equal("event"); 102 | _enqueue.args[0][1].should.have.keys("ec", "ea", "el") 103 | _enqueue.args[0][1].ec.should.equal(category); 104 | _enqueue.args[0][1].ea.should.equal(action); 105 | _enqueue.args[0][1].el.should.equal(label); 106 | 107 | fn.calledOnce.should.equal(true, "callback should have been called once") 108 | }); 109 | 110 | it("should accept arguments (category, action, label, value)", function () { 111 | var category = Math.random().toString(); 112 | var action = Math.random().toString(); 113 | var label = Math.random().toString(); 114 | var value = Math.random(); 115 | 116 | ua().event(category, action, label, value); 117 | 118 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 119 | _enqueue.args[0][0].should.equal("event"); 120 | _enqueue.args[0][1].should.have.keys("ec", "ea", "el", "ev") 121 | _enqueue.args[0][1].ec.should.equal(category); 122 | _enqueue.args[0][1].ea.should.equal(action); 123 | _enqueue.args[0][1].el.should.equal(label); 124 | _enqueue.args[0][1].ev.should.equal(value); 125 | }); 126 | 127 | it("should accept arguments (category, action, label, value, fn)", function () { 128 | var category = Math.random().toString(); 129 | var action = Math.random().toString(); 130 | var label = Math.random().toString(); 131 | var value = Math.random(); 132 | var fn = sinon.spy() 133 | 134 | ua().event(category, action, label, value, fn); 135 | 136 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 137 | _enqueue.args[0][0].should.equal("event"); 138 | _enqueue.args[0][1].should.have.keys("ec", "ea", "el", "ev") 139 | _enqueue.args[0][1].ec.should.equal(category); 140 | _enqueue.args[0][1].ea.should.equal(action); 141 | _enqueue.args[0][1].el.should.equal(label); 142 | _enqueue.args[0][1].ev.should.equal(value); 143 | 144 | fn.calledOnce.should.equal(true, "callback should have been called once") 145 | }); 146 | 147 | it("should accept arguments (category, action, label, value, params, fn)", function () { 148 | var category = Math.random().toString(); 149 | var action = Math.random().toString(); 150 | var label = Math.random().toString(); 151 | var value = Math.random(); 152 | var params = {"p": "/" + Math.random()} 153 | var fn = sinon.spy() 154 | 155 | ua().event(category, action, label, value, params, fn); 156 | 157 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 158 | _enqueue.args[0][0].should.equal("event"); 159 | _enqueue.args[0][1].should.have.keys("ec", "ea", "el", "ev", "p") 160 | _enqueue.args[0][1].ec.should.equal(category); 161 | _enqueue.args[0][1].ea.should.equal(action); 162 | _enqueue.args[0][1].el.should.equal(label); 163 | _enqueue.args[0][1].ev.should.equal(value); 164 | _enqueue.args[0][1].p.should.equal(params.p); 165 | 166 | fn.calledOnce.should.equal(true, "callback should have been called once") 167 | }); 168 | 169 | it("should accept arguments (params)", function () { 170 | var params = { 171 | ec: Math.random().toString(), 172 | ea: Math.random().toString(), 173 | el: Math.random().toString(), 174 | ev: Math.random(), 175 | "p": "/" + Math.random(), 176 | "empty": null 177 | } 178 | var json = JSON.stringify(params) 179 | 180 | ua().event(params); 181 | 182 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 183 | _enqueue.args[0][0].should.equal("event"); 184 | _enqueue.args[0][1].should.have.keys("ec", "ea", "el", "ev", "p") 185 | _enqueue.args[0][1].ec.should.equal(params.ec); 186 | _enqueue.args[0][1].ea.should.equal(params.ea); 187 | _enqueue.args[0][1].el.should.equal(params.el); 188 | _enqueue.args[0][1].ev.should.equal(params.ev); 189 | _enqueue.args[0][1].p.should.equal(params.p); 190 | 191 | JSON.stringify(params).should.equal(json, "params should not have been modified") 192 | }); 193 | 194 | it("should accept arguments (params, fn)", function () { 195 | var params = { 196 | ec: Math.random().toString(), 197 | ea: Math.random().toString(), 198 | el: Math.random().toString(), 199 | ev: Math.random(), 200 | "p": "/" + Math.random() 201 | } 202 | var fn = sinon.spy() 203 | 204 | ua().event(params, fn); 205 | 206 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 207 | _enqueue.args[0][0].should.equal("event"); 208 | _enqueue.args[0][1].should.have.keys("ec", "ea", "el", "ev", "p") 209 | _enqueue.args[0][1].ec.should.equal(params.ec); 210 | _enqueue.args[0][1].ea.should.equal(params.ea); 211 | _enqueue.args[0][1].el.should.equal(params.el); 212 | _enqueue.args[0][1].ev.should.equal(params.ev); 213 | _enqueue.args[0][1].p.should.equal(params.p); 214 | 215 | fn.calledOnce.should.equal(true, "callback should have been called once") 216 | }); 217 | 218 | it("should use the dp attribute as p for providing a event path", function () { 219 | var params = { 220 | ec: Math.random().toString(), 221 | ea: Math.random().toString(), 222 | "dp": "/" + Math.random(), 223 | } 224 | var json = JSON.stringify(params) 225 | 226 | ua().event(params); 227 | 228 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 229 | _enqueue.args[0][0].should.equal("event"); 230 | _enqueue.args[0][1].should.have.keys("ec", "ea", "p") 231 | _enqueue.args[0][1].ec.should.equal(params.ec); 232 | _enqueue.args[0][1].ea.should.equal(params.ea); 233 | _enqueue.args[0][1].p.should.equal(params.dp); 234 | 235 | JSON.stringify(params).should.equal(json, "params should not have been modified") 236 | }); 237 | 238 | 239 | it("should allow daisy-chaining and re-using parameters", function () { 240 | var params = { 241 | ec: Math.random().toString(), 242 | ea: Math.random().toString(), 243 | el: Math.random().toString(), 244 | ev: Math.random() 245 | } 246 | 247 | ua().event(params).event() 248 | 249 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for each event"); 250 | _enqueue.args[0][0].should.equal(_enqueue.args[1][0]); 251 | _enqueue.args[0][1].ec.should.equal(_enqueue.args[1][1].ec); 252 | _enqueue.args[0][1].ea.should.equal(_enqueue.args[1][1].ea); 253 | _enqueue.args[0][1].el.should.equal(_enqueue.args[1][1].el); 254 | _enqueue.args[0][1].ev.should.equal(_enqueue.args[1][1].ev); 255 | }); 256 | 257 | 258 | it("should extend and overwrite params when daisy-chaining", function () { 259 | var params = { 260 | ec: Math.random().toString(), 261 | ea: Math.random().toString(), 262 | el: Math.random().toString(), 263 | ev: Math.random() 264 | } 265 | var category = Math.random().toString(); 266 | 267 | ua().event(params).event(category) 268 | 269 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for each event"); 270 | _enqueue.args[0][0].should.equal(_enqueue.args[1][0]); 271 | _enqueue.args[0][1].ea.should.equal(_enqueue.args[1][1].ea); 272 | _enqueue.args[0][1].el.should.equal(_enqueue.args[1][1].el); 273 | _enqueue.args[0][1].ev.should.equal(_enqueue.args[1][1].ev); 274 | 275 | _enqueue.args[0][1].ec.should.equal(params.ec); 276 | _enqueue.args[1][1].ec.should.equal(category); 277 | }); 278 | 279 | it("should re-use the path when daisy-chained to a pageview", function () { 280 | var path = "/" + Math.random() 281 | var params = { 282 | ec: Math.random().toString(), 283 | ea: Math.random().toString(), 284 | el: Math.random().toString(), 285 | ev: Math.random() 286 | } 287 | 288 | ua().pageview(path).event(params).event(params); 289 | 290 | _enqueue.calledThrice.should.equal(true, "#_enqueue should have been called twice, once for the pageview, once for the pageview"); 291 | 292 | _enqueue.args[1][1].p.should.equal(path) 293 | _enqueue.args[2][1].p.should.equal(path) 294 | }) 295 | 296 | it("should fail without event category", function () { 297 | var fn = sinon.spy() 298 | var action = Math.random().toString(); 299 | var visitor = ua() 300 | 301 | var result = visitor.event(null, action, fn); 302 | 303 | visitor._context = result._context; 304 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 305 | 306 | result.should.be.instanceof(ua.Visitor); 307 | result._context.should.eql({}, "the transaction params should not be persisted") 308 | 309 | _enqueue.called.should.equal(false, "#_enqueue should have not been called once"); 310 | fn.calledOnce.should.equal(true, "callback should have been called once"); 311 | fn.args[0][0].should.be.instanceof(Error); 312 | fn.thisValues[0].should.equal(visitor); 313 | }); 314 | 315 | it("should fail without event action", function () { 316 | var fn = sinon.spy() 317 | var category = Math.random().toString(); 318 | var visitor = ua() 319 | 320 | var result = visitor.event(category, null, fn); 321 | 322 | visitor._context = result._context; 323 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 324 | 325 | result.should.be.instanceof(ua.Visitor); 326 | result._context.should.eql({}, "the transaction params should not be persisted") 327 | 328 | _enqueue.called.should.equal(false, "#_enqueue should have not been called once"); 329 | fn.calledOnce.should.equal(true, "callback should have been called once"); 330 | fn.args[0][0].should.be.instanceof(Error); 331 | fn.thisValues[0].should.equal(visitor); 332 | }); 333 | 334 | }); 335 | 336 | }); 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | -------------------------------------------------------------------------------- /test/exception.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require("querystring"); 3 | var uuid = require("uuid"); 4 | var should = require("should"); 5 | var sinon = require("sinon"); 6 | var url = require("url"); 7 | 8 | var ua = require("../lib/index.js"); 9 | var utils = require("../lib/utils.js"); 10 | var config = require("../lib/config.js"); 11 | var request = require("../lib/request"); 12 | 13 | 14 | describe("ua", function () { 15 | 16 | 17 | describe("#exception", function () { 18 | var _enqueue; 19 | 20 | beforeEach(function () { 21 | _enqueue = sinon.stub(ua.Visitor.prototype, "_enqueue", function () { 22 | if (arguments.length === 3 && typeof arguments[2] === 'function') { 23 | arguments[2](); 24 | } 25 | return this; 26 | }); 27 | }); 28 | 29 | afterEach(function () { 30 | _enqueue.restore() 31 | }); 32 | 33 | 34 | it("should accept arguments (description)", function () { 35 | var description = Math.random().toString(); 36 | 37 | var visitor = ua() 38 | 39 | var result = visitor.exception(description); 40 | 41 | visitor._context = result._context; 42 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 43 | 44 | result.should.be.instanceof(ua.Visitor); 45 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 46 | 47 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 48 | _enqueue.args[0][0].should.equal("exception"); 49 | _enqueue.args[0][1].should.have.keys("exd") 50 | _enqueue.args[0][1].exd.should.equal(description); 51 | }); 52 | 53 | 54 | it("should accept arguments (description, fn)", function () { 55 | var description = Math.random().toString(); 56 | var fn = sinon.spy(); 57 | 58 | ua().exception(description, fn); 59 | 60 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 61 | _enqueue.args[0][0].should.equal("exception"); 62 | _enqueue.args[0][1].should.have.keys("exd") 63 | _enqueue.args[0][1].exd.should.equal(description); 64 | 65 | fn.calledOnce.should.equal(true, "callback should have been called once") 66 | }); 67 | 68 | 69 | it("should accept arguments (description, fatal)", function () { 70 | var description = Math.random().toString(); 71 | var fatal = +!!(Math.random().toString()*2); 72 | 73 | var visitor = ua() 74 | 75 | var result = visitor.exception(description, fatal); 76 | 77 | visitor._context = result._context; 78 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 79 | 80 | result.should.be.instanceof(ua.Visitor); 81 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 82 | 83 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 84 | _enqueue.args[0][0].should.equal("exception"); 85 | _enqueue.args[0][1].should.have.keys("exd", "exf") 86 | _enqueue.args[0][1].exd.should.equal(description); 87 | _enqueue.args[0][1].exf.should.equal(fatal); 88 | }); 89 | 90 | 91 | it("should accept arguments (description, fatal, fn)", function () { 92 | var description = Math.random().toString(); 93 | var fatal = +!!(Math.random().toString()*2); 94 | var fn = sinon.spy(); 95 | 96 | var visitor = ua() 97 | 98 | var result = visitor.exception(description, fatal, fn); 99 | 100 | visitor._context = result._context; 101 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 102 | 103 | result.should.be.instanceof(ua.Visitor); 104 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 105 | 106 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 107 | _enqueue.args[0][0].should.equal("exception"); 108 | _enqueue.args[0][1].should.have.keys("exd", "exf") 109 | _enqueue.args[0][1].exd.should.equal(description); 110 | _enqueue.args[0][1].exf.should.equal(fatal); 111 | 112 | fn.calledOnce.should.equal(true, "callback should have been called once") 113 | }); 114 | 115 | 116 | it("should accept arguments (description, fatal, params)", function () { 117 | var description = Math.random().toString(); 118 | var fatal = +!!(Math.random().toString()*2); 119 | var params = {"p": "/" + Math.random()} 120 | 121 | var visitor = ua() 122 | 123 | var result = visitor.exception(description, fatal, params); 124 | 125 | visitor._context = result._context; 126 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 127 | 128 | result.should.be.instanceof(ua.Visitor); 129 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 130 | 131 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 132 | _enqueue.args[0][0].should.equal("exception"); 133 | _enqueue.args[0][1].should.have.keys("exd", "exf", "p") 134 | _enqueue.args[0][1].exd.should.equal(description); 135 | _enqueue.args[0][1].exf.should.equal(fatal); 136 | _enqueue.args[0][1].p.should.equal(params.p); 137 | }); 138 | 139 | 140 | it("should accept arguments (description, fatal, params, fn)", function () { 141 | var description = Math.random().toString(); 142 | var fatal = +!!(Math.random().toString()*2); 143 | var params = {"p": "/" + Math.random()}; 144 | var fn = sinon.spy(); 145 | 146 | var visitor = ua() 147 | 148 | var result = visitor.exception(description, fatal, params, fn); 149 | 150 | visitor._context = result._context; 151 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 152 | 153 | result.should.be.instanceof(ua.Visitor); 154 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 155 | 156 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 157 | _enqueue.args[0][0].should.equal("exception"); 158 | _enqueue.args[0][1].should.have.keys("exd", "exf", "p") 159 | _enqueue.args[0][1].exd.should.equal(description); 160 | _enqueue.args[0][1].exf.should.equal(fatal); 161 | _enqueue.args[0][1].p.should.equal(params.p); 162 | 163 | fn.calledOnce.should.equal(true, "callback should have been called once") 164 | }); 165 | 166 | 167 | it("should accept arguments (params)", function () { 168 | var params = { 169 | exd: Math.random().toString(), 170 | exf: +!!(Math.random().toString()*2), 171 | p: "/" + Math.random() 172 | }; 173 | 174 | var visitor = ua() 175 | 176 | var result = visitor.exception(params); 177 | 178 | visitor._context = result._context; 179 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 180 | 181 | result.should.be.instanceof(ua.Visitor); 182 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 183 | 184 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 185 | _enqueue.args[0][0].should.equal("exception"); 186 | _enqueue.args[0][1].should.have.keys("exd", "exf", "p") 187 | _enqueue.args[0][1].exd.should.equal(params.exd); 188 | _enqueue.args[0][1].exf.should.equal(params.exf); 189 | _enqueue.args[0][1].p.should.equal(params.p); 190 | }); 191 | 192 | 193 | it("should accept arguments (params, fn)", function () { 194 | var params = { 195 | exd: Math.random().toString(), 196 | exf: +!!(Math.random().toString()*2), 197 | p: "/" + Math.random() 198 | }; 199 | var fn = sinon.spy(); 200 | 201 | var visitor = ua() 202 | 203 | var result = visitor.exception(params, fn); 204 | 205 | visitor._context = result._context; 206 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 207 | 208 | result.should.be.instanceof(ua.Visitor); 209 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 210 | 211 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 212 | _enqueue.args[0][0].should.equal("exception"); 213 | _enqueue.args[0][1].should.have.keys("exd", "exf", "p") 214 | _enqueue.args[0][1].exd.should.equal(params.exd); 215 | _enqueue.args[0][1].exf.should.equal(params.exf); 216 | _enqueue.args[0][1].p.should.equal(params.p); 217 | 218 | fn.calledOnce.should.equal(true, "callback should have been called once") 219 | }); 220 | 221 | }); 222 | 223 | }); 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require("querystring"); 3 | var uuid = require("uuid"); 4 | var should = require("should"); 5 | var sinon = require("sinon"); 6 | var url = require("url"); 7 | 8 | var ua = require("../lib/index.js"); 9 | var utils = require("../lib/utils.js"); 10 | var config = require("../lib/config.js"); 11 | var request = require("../lib/request"); 12 | 13 | const v4Regex = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i); 14 | 15 | describe("ua", function () { 16 | 17 | it("should be usable as a function", function () { 18 | ua("foo").should.be.instanceof(ua.Visitor); 19 | }); 20 | 21 | it("should be usable as a constructor", function () { 22 | new ua("foo").should.be.instanceof(ua.Visitor); 23 | }); 24 | 25 | it("should accept arguments (tid, cid, options)", function () { 26 | var tid = "UA-XXXXX-XX" 27 | var cid = uuid.v4() 28 | var options = {}; 29 | 30 | var visitor = ua(tid, cid, options) 31 | 32 | visitor.tid.should.equal(tid) 33 | visitor.cid.should.equal(cid) 34 | visitor.options.should.equal(options) 35 | }); 36 | 37 | it("should accept arguments (tid, cid)", function () { 38 | var tid = "UA-XXXXX-XX" 39 | var cid = uuid.v4() 40 | 41 | var visitor = ua(tid, cid) 42 | 43 | visitor.tid.should.equal(tid) 44 | visitor.cid.should.equal(cid) 45 | visitor.options.should.eql({}, "An empty options hash should've been created") 46 | }); 47 | 48 | it("should accept arguments (tid, options)", function () { 49 | var tid = Math.random().toString(); 50 | var options = {} 51 | 52 | var visitor = ua(tid, options) 53 | 54 | visitor.tid.should.equal(tid) 55 | utils.isUuid(visitor.cid).should.equal(true, "A valid random UUID should have been generated") 56 | visitor.options.should.eql(options) 57 | 58 | }); 59 | 60 | it("should accept arguments (options)", function () { 61 | var options = {} 62 | 63 | var visitor = ua(options); 64 | 65 | visitor.should.have.property('tid', undefined); 66 | utils.isUuid(visitor.cid).should.equal(true, "A valid random UUID should have been generated") 67 | visitor.options.should.eql(options) 68 | }); 69 | 70 | it("should accept tid and cid via the options arguments", function () { 71 | var options = { 72 | tid: "UA-XXXXX-XX", 73 | cid: uuid.v4() 74 | }; 75 | 76 | var visitor = ua(options); 77 | 78 | visitor.tid.should.equal(options.tid) 79 | visitor.cid.should.equal(options.cid) 80 | visitor.options.should.equal(options) 81 | }); 82 | 83 | it("should generate new cid (UUID) if provided one is in wrong format", function () { 84 | var options = { 85 | tid: "UA-XXXXX-XX", 86 | cid: "custom-format-cid" 87 | }; 88 | 89 | var visitor = ua(options); 90 | 91 | visitor.cid.should.not.equal(options.cid) 92 | visitor.cid.should.match(v4Regex) 93 | }); 94 | 95 | it("should accept custom cid format when strictCidFormat is false", function () { 96 | var options = { 97 | tid: "UA-XXXXX-XX", 98 | cid: "custom-format-cid", 99 | strictCidFormat: false 100 | }; 101 | 102 | var visitor = ua(options); 103 | 104 | visitor.cid.should.equal(options.cid) 105 | }); 106 | 107 | 108 | describe("params", function () { 109 | 110 | var visitor; 111 | 112 | before(function () { 113 | var tid = "UA-XXXXX-XX"; 114 | var cid = uuid.v4(); 115 | visitor = ua(tid, cid); 116 | }); 117 | 118 | it('should not translate params', function(){ 119 | var params = { 120 | tid: 1, 121 | cid: 1, 122 | somefake: 1, 123 | v: 'a' 124 | }; 125 | 126 | visitor._translateParams(params).should.eql(params); 127 | }) 128 | 129 | it('should match all parameters and each should be in the list of accepted', function(){ 130 | var res = visitor._translateParams(config.parametersMap); 131 | for (var i in res) { 132 | if (res.hasOwnProperty(i)) { 133 | res[i].should.equal(i); 134 | config.acceptedParameters.should.containEql(i); 135 | } 136 | } 137 | }) 138 | 139 | }); 140 | 141 | }); 142 | -------------------------------------------------------------------------------- /test/item.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require("querystring"); 3 | var uuid = require("uuid"); 4 | var should = require("should"); 5 | var sinon = require("sinon"); 6 | var url = require("url"); 7 | 8 | var ua = require("../lib/index.js"); 9 | var utils = require("../lib/utils.js"); 10 | var config = require("../lib/config.js"); 11 | var request = require("../lib/request"); 12 | 13 | 14 | describe("ua", function () { 15 | 16 | 17 | describe("#item", function () { 18 | var _enqueue; 19 | 20 | beforeEach(function () { 21 | _enqueue = sinon.stub(ua.Visitor.prototype, "_enqueue", function (type, params, fn) { 22 | if (fn) { 23 | (typeof fn).should.equal('function', "#_enqueue should receive a callback") 24 | fn(); 25 | } 26 | return this; 27 | }); 28 | }); 29 | 30 | afterEach(function () { 31 | _enqueue.restore() 32 | }); 33 | 34 | 35 | it("should be available via the #i shortcut", function () { 36 | var visitor = ua() 37 | visitor.i.should.equal(visitor.item) 38 | }); 39 | 40 | 41 | it("should accept arguments (price) when chained to transaction", function () { 42 | var transaction = Math.random().toString(); 43 | var price = Math.random(); 44 | 45 | var visitor = ua().transaction(transaction); 46 | 47 | var result = visitor.item(price); 48 | 49 | visitor._context = result._context; 50 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 51 | result.should.be.instanceof(ua.Visitor); 52 | result._context.should.eql(_enqueue.args[1][1], "the transaction params should be persisted as the context of the visitor clone") 53 | 54 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for the transaction before"); 55 | _enqueue.args[0][0].should.equal("transaction"); 56 | _enqueue.args[1][0].should.equal("item"); 57 | _enqueue.args[1][1].should.have.keys("ti", "ip") 58 | _enqueue.args[1][1].ti.should.equal(transaction); 59 | _enqueue.args[1][1].ip.should.equal(price); 60 | }); 61 | 62 | 63 | it("should accept arguments (price, fn) when chained to transaction", function () { 64 | var transaction = Math.random().toString(); 65 | var price = Math.random(); 66 | var fn = sinon.spy() 67 | 68 | var visitor = ua().transaction(transaction); 69 | 70 | var result = visitor.item(price, fn); 71 | 72 | visitor._context = result._context; 73 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 74 | result.should.be.instanceof(ua.Visitor); 75 | result._context.should.eql(_enqueue.args[1][1], "the transaction params should be persisted as the context of the visitor clone") 76 | 77 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for the transaction before"); 78 | _enqueue.args[0][0].should.equal("transaction"); 79 | _enqueue.args[1][0].should.equal("item"); 80 | _enqueue.args[1][1].should.have.keys("ti", "ip") 81 | _enqueue.args[1][1].ti.should.equal(transaction); 82 | _enqueue.args[1][1].ip.should.equal(price); 83 | 84 | fn.calledOnce.should.equal(true, "callback should have been called once") 85 | }); 86 | 87 | 88 | it("should accept arguments (price, quantity) when chained to transaction", function () { 89 | var transaction = Math.random().toString(); 90 | var price = Math.random(); 91 | var quantity = Math.random(); 92 | 93 | var visitor = ua().transaction(transaction); 94 | 95 | var result = visitor.item(price, quantity); 96 | 97 | visitor._context = result._context; 98 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 99 | result.should.be.instanceof(ua.Visitor); 100 | result._context.should.eql(_enqueue.args[1][1], "the transaction params should be persisted as the context of the visitor clone") 101 | 102 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for the transaction before"); 103 | _enqueue.args[0][0].should.equal("transaction"); 104 | _enqueue.args[1][0].should.equal("item"); 105 | _enqueue.args[1][1].should.have.keys("ti", "ip", "iq") 106 | _enqueue.args[1][1].ti.should.equal(transaction); 107 | _enqueue.args[1][1].ip.should.equal(price); 108 | _enqueue.args[1][1].iq.should.equal(quantity); 109 | }); 110 | 111 | 112 | it("should accept arguments (price, quantity, fn) when chained to transaction", function () { 113 | var transaction = Math.random().toString(); 114 | var price = Math.random(); 115 | var quantity = Math.random(); 116 | var fn = sinon.spy() 117 | 118 | var visitor = ua().transaction(transaction); 119 | 120 | var result = visitor.item(price, quantity, fn); 121 | 122 | visitor._context = result._context; 123 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 124 | result.should.be.instanceof(ua.Visitor); 125 | result._context.should.eql(_enqueue.args[1][1], "the transaction params should be persisted as the context of the visitor clone") 126 | 127 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for the transaction before"); 128 | _enqueue.args[0][0].should.equal("transaction"); 129 | _enqueue.args[1][0].should.equal("item"); 130 | _enqueue.args[1][1].should.have.keys("ti", "ip", "iq") 131 | _enqueue.args[1][1].ti.should.equal(transaction); 132 | _enqueue.args[1][1].ip.should.equal(price); 133 | _enqueue.args[1][1].iq.should.equal(quantity); 134 | 135 | fn.calledOnce.should.equal(true, "callback should have been called once") 136 | }); 137 | 138 | 139 | it("should accept arguments (price, quantity, sku) when chained to transaction", function () { 140 | var transaction = Math.random().toString(); 141 | var price = Math.random(); 142 | var quantity = Math.random(); 143 | var sku = Math.random().toString(); 144 | 145 | var visitor = ua().transaction(transaction); 146 | 147 | var result = visitor.item(price, quantity, sku); 148 | 149 | visitor._context = result._context; 150 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 151 | result.should.be.instanceof(ua.Visitor); 152 | result._context.should.eql(_enqueue.args[1][1], "the transaction params should be persisted as the context of the visitor clone") 153 | 154 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for the transaction before"); 155 | _enqueue.args[0][0].should.equal("transaction"); 156 | _enqueue.args[1][0].should.equal("item"); 157 | _enqueue.args[1][1].should.have.keys("ti", "ip", "iq", "ic") 158 | _enqueue.args[1][1].ti.should.equal(transaction); 159 | _enqueue.args[1][1].ip.should.equal(price); 160 | _enqueue.args[1][1].iq.should.equal(quantity); 161 | _enqueue.args[1][1].ic.should.equal(sku); 162 | }); 163 | 164 | 165 | it("should accept arguments (price, quantity, sku, fn) when chained to transaction", function () { 166 | var transaction = Math.random().toString(); 167 | var price = Math.random(); 168 | var quantity = Math.random(); 169 | var sku = Math.random().toString(); 170 | var fn = sinon.spy() 171 | 172 | var visitor = ua().transaction(transaction); 173 | 174 | var result = visitor.item(price, quantity, sku, fn); 175 | 176 | visitor._context = result._context; 177 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 178 | result.should.be.instanceof(ua.Visitor); 179 | result._context.should.eql(_enqueue.args[1][1], "the transaction params should be persisted as the context of the visitor clone") 180 | 181 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for the transaction before"); 182 | _enqueue.args[0][0].should.equal("transaction"); 183 | _enqueue.args[1][0].should.equal("item"); 184 | _enqueue.args[1][1].should.have.keys("ti", "ip", "iq", "ic") 185 | _enqueue.args[1][1].ti.should.equal(transaction); 186 | _enqueue.args[1][1].ip.should.equal(price); 187 | _enqueue.args[1][1].iq.should.equal(quantity); 188 | _enqueue.args[1][1].ic.should.equal(sku); 189 | 190 | fn.calledOnce.should.equal(true, "callback should have been called once") 191 | }); 192 | 193 | 194 | it("should accept arguments (price, quantity, sku, name) when chained to transaction", function () { 195 | var transaction = Math.random().toString(); 196 | var price = Math.random(); 197 | var quantity = Math.random(); 198 | var sku = Math.random().toString(); 199 | var name = Math.random().toString(); 200 | 201 | var visitor = ua().transaction(transaction); 202 | 203 | var result = visitor.item(price, quantity, sku, name); 204 | 205 | visitor._context = result._context; 206 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 207 | result.should.be.instanceof(ua.Visitor); 208 | result._context.should.eql(_enqueue.args[1][1], "the transaction params should be persisted as the context of the visitor clone") 209 | 210 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for the transaction before"); 211 | _enqueue.args[0][0].should.equal("transaction"); 212 | _enqueue.args[1][0].should.equal("item"); 213 | _enqueue.args[1][1].should.have.keys("ti", "ip", "iq", "ic", "in") 214 | _enqueue.args[1][1].ti.should.equal(transaction); 215 | _enqueue.args[1][1].ip.should.equal(price); 216 | _enqueue.args[1][1].iq.should.equal(quantity); 217 | _enqueue.args[1][1].ic.should.equal(sku); 218 | _enqueue.args[1][1].in.should.equal(name); 219 | }); 220 | 221 | 222 | it("should accept arguments (price, quantity, sku, name, fn) when chained to transaction", function () { 223 | var transaction = Math.random().toString(); 224 | var price = Math.random(); 225 | var quantity = Math.random(); 226 | var sku = Math.random().toString(); 227 | var name = Math.random().toString(); 228 | var fn = sinon.spy() 229 | 230 | var visitor = ua().transaction(transaction); 231 | 232 | var result = visitor.item(price, quantity, sku, name, fn); 233 | 234 | visitor._context = result._context; 235 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 236 | result.should.be.instanceof(ua.Visitor); 237 | result._context.should.eql(_enqueue.args[1][1], "the transaction params should be persisted as the context of the visitor clone") 238 | 239 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for the transaction before"); 240 | _enqueue.args[0][0].should.equal("transaction"); 241 | _enqueue.args[1][0].should.equal("item"); 242 | _enqueue.args[1][1].should.have.keys("ti", "ip", "iq", "ic", "in") 243 | _enqueue.args[1][1].ti.should.equal(transaction); 244 | _enqueue.args[1][1].ip.should.equal(price); 245 | _enqueue.args[1][1].iq.should.equal(quantity); 246 | _enqueue.args[1][1].ic.should.equal(sku); 247 | _enqueue.args[1][1].in.should.equal(name); 248 | 249 | fn.calledOnce.should.equal(true, "callback should have been called once") 250 | }); 251 | 252 | 253 | it("should accept arguments (price, quantity, sku, name, variation) when chained to transaction", function () { 254 | var transaction = Math.random().toString(); 255 | var price = Math.random(); 256 | var quantity = Math.random(); 257 | var sku = Math.random().toString(); 258 | var name = Math.random().toString(); 259 | var variation = Math.random().toString(); 260 | 261 | var visitor = ua().transaction(transaction); 262 | 263 | var result = visitor.item(price, quantity, sku, name, variation); 264 | 265 | visitor._context = result._context; 266 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 267 | result.should.be.instanceof(ua.Visitor); 268 | result._context.should.eql(_enqueue.args[1][1], "the transaction params should be persisted as the context of the visitor clone") 269 | 270 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for the transaction before"); 271 | _enqueue.args[0][0].should.equal("transaction"); 272 | _enqueue.args[1][0].should.equal("item"); 273 | _enqueue.args[1][1].should.have.keys("ti", "ip", "iq", "ic", "in", "iv") 274 | _enqueue.args[1][1].ti.should.equal(transaction); 275 | _enqueue.args[1][1].ip.should.equal(price); 276 | _enqueue.args[1][1].iq.should.equal(quantity); 277 | _enqueue.args[1][1].ic.should.equal(sku); 278 | _enqueue.args[1][1].in.should.equal(name); 279 | _enqueue.args[1][1].iv.should.equal(variation); 280 | }); 281 | 282 | 283 | it("should accept arguments (price, quantity, sku, name, variation, fn) when chained to transaction", function () { 284 | var transaction = Math.random().toString(); 285 | var price = Math.random(); 286 | var quantity = Math.random(); 287 | var sku = Math.random().toString(); 288 | var name = Math.random().toString(); 289 | var variation = Math.random().toString(); 290 | var fn = sinon.spy() 291 | 292 | var visitor = ua().transaction(transaction); 293 | 294 | var result = visitor.item(price, quantity, sku, name, variation, fn); 295 | 296 | visitor._context = result._context; 297 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 298 | result.should.be.instanceof(ua.Visitor); 299 | result._context.should.eql(_enqueue.args[1][1], "the transaction params should be persisted as the context of the visitor clone") 300 | 301 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for the transaction before"); 302 | _enqueue.args[0][0].should.equal("transaction"); 303 | _enqueue.args[1][0].should.equal("item"); 304 | _enqueue.args[1][1].should.have.keys("ti", "ip", "iq", "ic", "in", "iv") 305 | _enqueue.args[1][1].ti.should.equal(transaction); 306 | _enqueue.args[1][1].ip.should.equal(price); 307 | _enqueue.args[1][1].iq.should.equal(quantity); 308 | _enqueue.args[1][1].ic.should.equal(sku); 309 | _enqueue.args[1][1].in.should.equal(name); 310 | _enqueue.args[1][1].iv.should.equal(variation); 311 | 312 | fn.calledOnce.should.equal(true, "callback should have been called once") 313 | }); 314 | 315 | 316 | it("should accept arguments (price, quantity, sku, name, variation, params) when chained to transaction", function () { 317 | var transaction = Math.random().toString(); 318 | var price = Math.random(); 319 | var quantity = Math.random(); 320 | var sku = Math.random().toString(); 321 | var name = Math.random().toString(); 322 | var variation = Math.random().toString(); 323 | var params = {foo: Math.random().toString()}; 324 | 325 | var visitor = ua().transaction(transaction); 326 | 327 | var result = visitor.item(price, quantity, sku, name, variation, params); 328 | 329 | visitor._context = result._context; 330 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 331 | result.should.be.instanceof(ua.Visitor); 332 | result._context.should.eql(_enqueue.args[1][1], "the transaction params should be persisted as the context of the visitor clone") 333 | 334 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for the transaction before"); 335 | _enqueue.args[0][0].should.equal("transaction"); 336 | _enqueue.args[1][0].should.equal("item"); 337 | _enqueue.args[1][1].should.have.keys("ti", "ip", "iq", "ic", "in", "iv", "foo") 338 | _enqueue.args[1][1].ti.should.equal(transaction); 339 | _enqueue.args[1][1].ip.should.equal(price); 340 | _enqueue.args[1][1].iq.should.equal(quantity); 341 | _enqueue.args[1][1].ic.should.equal(sku); 342 | _enqueue.args[1][1].in.should.equal(name); 343 | _enqueue.args[1][1].iv.should.equal(variation); 344 | _enqueue.args[1][1].foo.should.equal(params.foo); 345 | }); 346 | 347 | 348 | it("should accept arguments (price, quantity, sku, name, variation, params, fn) when chained to transaction", function () { 349 | var transaction = Math.random().toString(); 350 | var price = Math.random(); 351 | var quantity = Math.random(); 352 | var sku = Math.random().toString(); 353 | var name = Math.random().toString(); 354 | var variation = Math.random().toString(); 355 | var params = {foo: Math.random().toString()}; 356 | var fn = sinon.spy() 357 | 358 | var visitor = ua().transaction(transaction); 359 | 360 | var result = visitor.item(price, quantity, sku, name, variation, params, fn); 361 | 362 | visitor._context = result._context; 363 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 364 | result.should.be.instanceof(ua.Visitor); 365 | result._context.should.eql(_enqueue.args[1][1], "the transaction params should be persisted as the context of the visitor clone") 366 | 367 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for the transaction before"); 368 | _enqueue.args[0][0].should.equal("transaction"); 369 | _enqueue.args[1][0].should.equal("item"); 370 | _enqueue.args[1][1].should.have.keys("ti", "ip", "iq", "ic", "in", "iv", "foo") 371 | _enqueue.args[1][1].ti.should.equal(transaction); 372 | _enqueue.args[1][1].ip.should.equal(price); 373 | _enqueue.args[1][1].iq.should.equal(quantity); 374 | _enqueue.args[1][1].ic.should.equal(sku); 375 | _enqueue.args[1][1].in.should.equal(name); 376 | _enqueue.args[1][1].iv.should.equal(variation); 377 | _enqueue.args[1][1].foo.should.equal(params.foo); 378 | 379 | fn.calledOnce.should.equal(true, "callback should have been called once") 380 | }); 381 | 382 | 383 | it("should accept arguments (price, quantity, sku, name, variation, params, fn) when params include transaction", function () { 384 | var price = Math.random(); 385 | var quantity = Math.random(); 386 | var sku = Math.random().toString(); 387 | var name = Math.random().toString(); 388 | var variation = Math.random().toString(); 389 | var params = {ti: Math.random()}; 390 | var fn = sinon.spy() 391 | 392 | var visitor = ua(); 393 | 394 | var result = visitor.item(price, quantity, sku, name, variation, params, fn); 395 | 396 | visitor._context = result._context; 397 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 398 | result.should.be.instanceof(ua.Visitor); 399 | result._context.should.eql(_enqueue.args[0][1], "the transaction params should be persisted as the context of the visitor clone") 400 | 401 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 402 | _enqueue.args[0][0].should.equal("item"); 403 | _enqueue.args[0][1].should.have.keys("ti", "ip", "iq", "ic", "in", "iv") 404 | _enqueue.args[0][1].ip.should.equal(price); 405 | _enqueue.args[0][1].iq.should.equal(quantity); 406 | _enqueue.args[0][1].ic.should.equal(sku); 407 | _enqueue.args[0][1].in.should.equal(name); 408 | _enqueue.args[0][1].iv.should.equal(variation); 409 | _enqueue.args[0][1].ti.should.equal(params.ti); 410 | 411 | fn.calledOnce.should.equal(true, "callback should have been called once") 412 | }); 413 | 414 | 415 | it("should accept arguments (params, fn) when params include transaction", function () { 416 | var params = { 417 | ip: Math.random(), 418 | iq: Math.random(), 419 | ic: Math.random().toString(), 420 | in: Math.random().toString(), 421 | iv: Math.random().toString(), 422 | ti: Math.random() 423 | }; 424 | var fn = sinon.spy() 425 | 426 | var visitor = ua(); 427 | 428 | var result = visitor.item(params, fn); 429 | 430 | visitor._context = result._context; 431 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 432 | result.should.be.instanceof(ua.Visitor); 433 | result._context.should.eql(_enqueue.args[0][1], "the transaction params should be persisted as the context of the visitor clone") 434 | 435 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 436 | _enqueue.args[0][0].should.equal("item"); 437 | _enqueue.args[0][1].should.have.keys("ti", "ip", "iq", "ic", "in", "iv") 438 | _enqueue.args[0][1].ip.should.equal(params.ip); 439 | _enqueue.args[0][1].iq.should.equal(params.iq); 440 | _enqueue.args[0][1].ic.should.equal(params.ic); 441 | _enqueue.args[0][1].in.should.equal(params.in); 442 | _enqueue.args[0][1].iv.should.equal(params.iv); 443 | _enqueue.args[0][1].ti.should.equal(params.ti); 444 | 445 | fn.calledOnce.should.equal(true, "callback should have been called once") 446 | }); 447 | 448 | 449 | it("should accept arguments (params, fn) when chained to a transaction", function () { 450 | var transaction = Math.random().toString(); 451 | var params = { 452 | ip: Math.random(), 453 | iq: Math.random(), 454 | ic: Math.random().toString(), 455 | in: Math.random().toString(), 456 | iv: Math.random().toString() 457 | }; 458 | var fn = sinon.spy() 459 | 460 | var visitor = ua().transaction(transaction); 461 | 462 | var result = visitor.item(params, fn); 463 | 464 | visitor._context = result._context; 465 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 466 | result.should.be.instanceof(ua.Visitor); 467 | result._context.should.eql(_enqueue.args[1][1], "the transaction params should be persisted as the context of the visitor clone") 468 | 469 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for the transaction"); 470 | _enqueue.args[1][0].should.equal("item"); 471 | _enqueue.args[1][1].should.have.keys("ti", "ip", "iq", "ic", "in", "iv") 472 | _enqueue.args[1][1].ip.should.equal(params.ip); 473 | _enqueue.args[1][1].iq.should.equal(params.iq); 474 | _enqueue.args[1][1].ic.should.equal(params.ic); 475 | _enqueue.args[1][1].in.should.equal(params.in); 476 | _enqueue.args[1][1].iv.should.equal(params.iv); 477 | _enqueue.args[1][1].ti.should.equal(transaction); 478 | 479 | fn.calledOnce.should.equal(true, "callback should have been called once") 480 | }); 481 | 482 | 483 | it("should allow daisy-chaining to re-use arguments", function () { 484 | var transaction = Math.random().toString(); 485 | var price = Math.random(); 486 | var quantity = Math.random(); 487 | var sku = Math.random().toString(); 488 | var name = Math.random().toString(); 489 | var name2 = Math.random().toString(); 490 | var variation = Math.random().toString(); 491 | var fn = sinon.spy() 492 | 493 | var visitor = ua().transaction(transaction); 494 | 495 | var result = visitor.item(price, quantity, sku, name, variation).item({in: name2}, fn); 496 | 497 | _enqueue.calledThrice.should.equal(true, "#_enqueue should have been called thrice, once for the transaction before"); 498 | _enqueue.args[0][0].should.equal("transaction"); 499 | 500 | _enqueue.args[1][0].should.equal(_enqueue.args[2][0]); 501 | _enqueue.args[1][1].ti.should.equal(_enqueue.args[2][1].ti, "ti should be equal on both"); 502 | _enqueue.args[1][1].ip.should.equal(_enqueue.args[2][1].ip, "ip should be equal on both"); 503 | _enqueue.args[1][1].iq.should.equal(_enqueue.args[2][1].iq, "iq should be equal on both"); 504 | _enqueue.args[1][1].ic.should.equal(_enqueue.args[2][1].ic, "ic should be equal on both"); 505 | _enqueue.args[1][1].iv.should.equal(_enqueue.args[2][1].iv, "iv should be equal on both"); 506 | _enqueue.args[2][1].in.should.equal(name2, "name should have changed on second item"); 507 | 508 | fn.calledOnce.should.equal(true, "callback should have been called once") 509 | }); 510 | 511 | 512 | it("should fail without transaction ID", function () { 513 | var params = { 514 | ip: Math.random(), 515 | iq: Math.random(), 516 | ic: Math.random().toString(), 517 | in: Math.random().toString(), 518 | iv: Math.random().toString() 519 | }; 520 | var fn = sinon.spy() 521 | 522 | var visitor = ua(); 523 | 524 | var result = visitor.item(params, fn); 525 | 526 | visitor._context = result._context; 527 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 528 | 529 | result.should.be.instanceof(ua.Visitor); 530 | result._context.should.eql({}, "the transaction params should not be persisted") 531 | 532 | _enqueue.called.should.equal(false, "#_enqueue should have not been called once"); 533 | fn.calledOnce.should.equal(true, "callback should have been called once"); 534 | fn.args[0][0].should.be.instanceof(Error); 535 | fn.thisValues[0].should.equal(visitor); 536 | }); 537 | 538 | }); 539 | 540 | }); 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | -------------------------------------------------------------------------------- /test/middleware.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require("querystring"); 3 | var uuid = require("uuid"); 4 | var should = require("should"); 5 | var sinon = require("sinon"); 6 | var url = require("url"); 7 | 8 | var ua = require("../lib/index.js"); 9 | var utils = require("../lib/utils.js"); 10 | var config = require("../lib/config.js"); 11 | var request = require("../lib/request"); 12 | 13 | 14 | describe("ua", function () { 15 | 16 | 17 | describe("middleware", function () { 18 | 19 | it("should return a middleware-capable function", function () { 20 | var middleware = ua.middleware() 21 | middleware.length.should.equal(3, "function signature should have three arguments") 22 | }); 23 | 24 | it("should try to create a visitor based on session data", function () { 25 | var req = {session: {cid: uuid.v4()}} 26 | var options = {debug: true} 27 | var middleware = ua.middleware("tid", options) 28 | var next = sinon.spy() 29 | 30 | middleware(req, {}, next) 31 | 32 | next.calledOnce.should.equal(true, "next() should've been called once") 33 | req.visitor.should.be.instanceof(ua.Visitor, "The request should have gained a UA visitor instance") 34 | req.visitor.cid.should.equal(req.session.cid, "The client ID should've been propagated") 35 | req.visitor.tid.should.equal("tid", "The tracking ID should've been propagated") 36 | req.visitor.options.should.eql(options, "The options should've been proprapated") 37 | }); 38 | 39 | 40 | it("should try to create a visitor based on the _ga cookie", function () { 41 | var req = {cookies: {_ga: "GA1.2.46218180.1366882461"}} 42 | var options = {debug: false} 43 | var middleware = ua.middleware("tid", options) 44 | var next = sinon.spy() 45 | 46 | middleware(req, {}, next) 47 | 48 | next.calledOnce.should.equal(true, "next() should've been called once") 49 | req.visitor.should.be.instanceof(ua.Visitor, "The request should have gained a UA visitor instance") 50 | req.visitor.cid.should.equal("46218180.1366882461", "The client ID should've been extracted from the cookie") 51 | req.visitor.tid.should.equal("tid", "The tracking ID should've been propagated") 52 | req.visitor.options.should.eql(options, "The options should've been proprapated") 53 | }) 54 | 55 | 56 | it("should allow changing the _ga cookie name", function () { 57 | var req = {cookies: {foo: "GA1.2.46218180.1366882461"}} 58 | var options = {cookieName: "foo"} 59 | var middleware = ua.middleware("tid", options) 60 | var next = sinon.spy() 61 | 62 | middleware(req, {}, next) 63 | 64 | req.visitor.cid.should.equal("46218180.1366882461", "The client ID should've been extracted from the cookie") 65 | }) 66 | 67 | it("should allow changing the variable name on req", function() { 68 | var req = {cookies: {_ga: "GA1.2.46218180.1366882461"}} 69 | var options = {instanceName: "banana"} 70 | var middleware = ua.middleware("tid", options) 71 | var next = sinon.spy() 72 | 73 | middleware(req, {}, next) 74 | 75 | req.banana.cid.should.equal("46218180.1366882461", "The request should have the UA visitor instance on the non-default variable name") 76 | }) 77 | 78 | 79 | it("should store the cid in the session", function () { 80 | var req = {cookies: {_ga: "GA1.2.46218180.1366882461"}, session: {}} 81 | var options = {debug: false} 82 | var middleware = ua.middleware("tid", options) 83 | var next = sinon.spy() 84 | 85 | middleware(req, {}, next) 86 | 87 | req.session.cid.should.equal("46218180.1366882461", "The client ID should've saved to the session") 88 | }) 89 | }); 90 | 91 | describe("createFromSession", function () { 92 | 93 | it("should combine an existing tracking ID with a client ID from the session", function () { 94 | var options = {debug: false} 95 | var middleware = ua.middleware("tid", options) 96 | var session = {cid: uuid.v4()} 97 | 98 | var visitor = ua.createFromSession(session); 99 | 100 | visitor.should.be.instanceof(ua.Visitor, "The request should have gained a UA visitor instance") 101 | visitor.cid.should.equal(session.cid, "The client ID should've been extracted from the cookie") 102 | visitor.tid.should.equal("tid", "The tracking ID should've been propagated") 103 | visitor.options.should.eql(options, "The options should've been proprapated") 104 | }) 105 | 106 | }) 107 | 108 | 109 | }); 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --reporter dot 3 | --ui bdd 4 | -------------------------------------------------------------------------------- /test/pageview.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require("querystring"); 3 | var uuid = require("uuid"); 4 | var should = require("should"); 5 | var sinon = require("sinon"); 6 | var url = require("url"); 7 | 8 | var ua = require("../lib/index.js"); 9 | var utils = require("../lib/utils.js"); 10 | var config = require("../lib/config.js"); 11 | var request = require("../lib/request"); 12 | 13 | 14 | describe("ua", function () { 15 | 16 | 17 | 18 | describe("#pageview", function () { 19 | var _enqueue; 20 | 21 | beforeEach(function () { 22 | _enqueue = sinon.stub(ua.Visitor.prototype, "_enqueue", function () { 23 | if (arguments.length === 3 && typeof arguments[2] === 'function') { 24 | arguments[2](); 25 | } 26 | return this; 27 | }); 28 | }); 29 | 30 | afterEach(function () { 31 | _enqueue.restore() 32 | }); 33 | 34 | 35 | it("should be available via the #pv shortcut", function () { 36 | var visitor = ua() 37 | visitor.pv.should.equal(visitor.pageview) 38 | }); 39 | 40 | 41 | it("should accept arguments (path)", function () { 42 | var path = "/" + Math.random() 43 | 44 | var visitor = ua("UA-XXXXX-XX") 45 | 46 | var result = visitor.pageview(path) 47 | 48 | visitor._context = result._context; 49 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 50 | 51 | result.should.be.instanceof(ua.Visitor); 52 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 53 | 54 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 55 | _enqueue.args[0][0].should.equal("pageview"); 56 | _enqueue.args[0][1].should.have.keys("dp") 57 | _enqueue.args[0][1].dp.should.equal(path); 58 | }); 59 | 60 | 61 | it("should accept arguments (path, fn)", function () { 62 | var path = "/" + Math.random(); 63 | var fn = sinon.spy(); 64 | 65 | var visitor = ua("UA-XXXXX-XX") 66 | 67 | var result = visitor.pageview(path, fn) 68 | 69 | visitor._context = result._context; 70 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 71 | 72 | result.should.be.instanceof(ua.Visitor); 73 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 74 | 75 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 76 | _enqueue.args[0][0].should.equal("pageview"); 77 | _enqueue.args[0][1].should.have.keys("dp") 78 | _enqueue.args[0][1].dp.should.equal(path); 79 | 80 | fn.calledOnce.should.equal(true, "callback should have been called once"); 81 | }); 82 | 83 | 84 | it("should accept arguments (params)", function () { 85 | var params = { 86 | dp: "/" + Math.random() 87 | }; 88 | 89 | var visitor = ua("UA-XXXXX-XX") 90 | 91 | var result = visitor.pageview(params) 92 | 93 | visitor._context = result._context; 94 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 95 | 96 | result.should.be.instanceof(ua.Visitor); 97 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 98 | 99 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 100 | _enqueue.args[0][0].should.equal("pageview"); 101 | _enqueue.args[0][1].should.have.keys("dp") 102 | _enqueue.args[0][1].dp.should.equal(params.dp); 103 | }); 104 | 105 | 106 | it("should accept arguments (params, fn)", function () { 107 | var params = { 108 | dp: "/" + Math.random(), 109 | empty: null // Should be removed 110 | }; 111 | var json = JSON.stringify(params) 112 | var fn = sinon.spy() 113 | 114 | ua("UA-XXXXX-XX").pageview(params, fn) 115 | 116 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 117 | _enqueue.args[0][0].should.equal("pageview"); 118 | _enqueue.args[0][1].should.have.keys("dp") 119 | _enqueue.args[0][1].dp.should.equal(params.dp); 120 | 121 | fn.calledOnce.should.equal(true, "callback should have been called once"); 122 | 123 | JSON.stringify(params).should.equal(json, "params should not have been modified") 124 | }); 125 | 126 | 127 | it("should accept arguments (path, hostname)", function () { 128 | var path = Math.random().toString(); 129 | var hostname = Math.random().toString(); 130 | 131 | ua("UA-XXXXX-XX").pageview(path, hostname); 132 | 133 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 134 | _enqueue.args[0][0].should.equal("pageview"); 135 | _enqueue.args[0][1].should.have.keys("dp", "dh") 136 | _enqueue.args[0][1].dp.should.equal(path); 137 | _enqueue.args[0][1].dh.should.equal(hostname); 138 | }); 139 | 140 | 141 | it("should accept arguments (path, hostname, fn)", function () { 142 | var path = Math.random().toString(); 143 | var hostname = Math.random().toString(); 144 | var fn = sinon.spy() 145 | 146 | ua("UA-XXXXX-XX").pageview(path, hostname, fn); 147 | 148 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 149 | _enqueue.args[0][0].should.equal("pageview"); 150 | _enqueue.args[0][1].should.have.keys("dp", "dh") 151 | _enqueue.args[0][1].dp.should.equal(path); 152 | _enqueue.args[0][1].dh.should.equal(hostname); 153 | 154 | fn.calledOnce.should.equal(true, "callback should have been called once"); 155 | }); 156 | 157 | 158 | it("should accept arguments (path, hostname, title)", function () { 159 | var path = Math.random().toString(); 160 | var hostname = Math.random().toString(); 161 | var title = Math.random().toString(); 162 | 163 | ua("UA-XXXXX-XX").pageview(path, hostname, title); 164 | 165 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 166 | _enqueue.args[0][0].should.equal("pageview"); 167 | _enqueue.args[0][1].should.have.keys("dp", "dh", "dt") 168 | _enqueue.args[0][1].dp.should.equal(path); 169 | _enqueue.args[0][1].dh.should.equal(hostname); 170 | _enqueue.args[0][1].dt.should.equal(title); 171 | }); 172 | 173 | 174 | it("should accept arguments (path, hostname, title, fn)", function () { 175 | var path = Math.random().toString(); 176 | var hostname = Math.random().toString(); 177 | var title = Math.random().toString(); 178 | var fn = sinon.spy() 179 | 180 | ua("UA-XXXXX-XX").pageview(path, hostname, title, fn); 181 | 182 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 183 | _enqueue.args[0][0].should.equal("pageview"); 184 | _enqueue.args[0][1].should.have.keys("dp", "dh", "dt") 185 | _enqueue.args[0][1].dp.should.equal(path); 186 | _enqueue.args[0][1].dh.should.equal(hostname); 187 | _enqueue.args[0][1].dt.should.equal(title); 188 | 189 | fn.calledOnce.should.equal(true, "callback should have been called once"); 190 | }); 191 | 192 | 193 | it("should allow daisy-chaining and re-using parameters", function () { 194 | var path = "/" + Math.random() 195 | var title = Math.random().toString(); 196 | 197 | ua("UA-XXXXX-XX").pageview(path, null, title).pageview() 198 | 199 | _enqueue.calledTwice.should.equal(true, "#_enqueue should have been called twice, once for each pageview"); 200 | 201 | _enqueue.args[0][0].should.equal(_enqueue.args[1][0]); 202 | _enqueue.args[0][1].should.eql(_enqueue.args[1][1]); 203 | }) 204 | 205 | 206 | it("should extend and overwrite params when daisy-chaining", function () { 207 | var path = "/" + Math.random() 208 | var path2 = "/" + Math.random() 209 | var title = Math.random().toString(); 210 | var title2 = Math.random().toString(); 211 | var foo = Math.random() 212 | 213 | ua("UA-XXXXX-XX") 214 | .pageview(path, null, title) 215 | .pageview({ 216 | dt: title2, 217 | dp: path2, 218 | foo: foo 219 | }).pageview(path) 220 | 221 | _enqueue.calledThrice.should.equal(true, "#_enqueue should have been called three times, once for each pageview"); 222 | 223 | _enqueue.args[0][1].should.have.keys("dp", "dt") 224 | _enqueue.args[0][1].dp.should.equal(path); 225 | _enqueue.args[0][1].dt.should.equal(title); 226 | 227 | _enqueue.args[1][1].should.have.keys("dp", "dt", "foo") 228 | _enqueue.args[1][1].dp.should.equal(path2); 229 | _enqueue.args[1][1].dt.should.equal(title2); 230 | _enqueue.args[1][1].foo.should.equal(foo); 231 | 232 | _enqueue.args[2][1].should.have.keys("dp", "dt") 233 | _enqueue.args[2][1].dp.should.equal(path); 234 | _enqueue.args[2][1].dt.should.equal(title2); 235 | }) 236 | 237 | it("should accept document location instead of page path", function () { 238 | var params = { 239 | dl: "http://www.google.com/" + Math.random() 240 | }; 241 | var json = JSON.stringify(params) 242 | var fn = sinon.spy() 243 | 244 | ua("UA-XXXXX-XX").pageview(params, fn) 245 | 246 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 247 | _enqueue.args[0][0].should.equal("pageview"); 248 | _enqueue.args[0][1].should.have.keys("dl") 249 | _enqueue.args[0][1].dl.should.equal(params.dl); 250 | 251 | fn.calledOnce.should.equal(true, "callback should have been called once"); 252 | 253 | JSON.stringify(params).should.equal(json, "params should not have been modified") 254 | }); 255 | 256 | it("should fail without page path or document location", function () { 257 | var fn = sinon.spy() 258 | var visitor = ua() 259 | 260 | var result = visitor.pageview(null, fn); 261 | 262 | visitor._context = result._context; 263 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 264 | 265 | result.should.be.instanceof(ua.Visitor); 266 | result._context.should.eql({}, "the transaction params should not be persisted") 267 | 268 | _enqueue.called.should.equal(false, "#_enqueue should have not been called once"); 269 | fn.calledOnce.should.equal(true, "callback should have been called once"); 270 | fn.args[0][0].should.be.instanceof(Error); 271 | fn.thisValues[0].should.equal(visitor); 272 | }); 273 | 274 | }); 275 | 276 | }); 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | -------------------------------------------------------------------------------- /test/send.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require("querystring"); 3 | var uuid = require("uuid"); 4 | var should = require("should"); 5 | var sinon = require("sinon"); 6 | var url = require("url"); 7 | 8 | var ua = require("../lib/index.js"); 9 | var utils = require("../lib/utils.js") 10 | var config = require("../lib/config.js") 11 | var request = require("../lib/request.js"); 12 | 13 | 14 | describe("ua", function () { 15 | 16 | describe("#send", function () { 17 | var post; 18 | 19 | beforeEach(function () { 20 | post = sinon.stub(request, "post").callsArg(3); 21 | }); 22 | 23 | afterEach(function () { 24 | post.restore() 25 | }); 26 | 27 | it("should immidiately return with an empty queue", function () { 28 | var visitor = ua(); 29 | var fn = sinon.spy(); 30 | 31 | visitor.send(fn); 32 | 33 | post.called.should.equal(false, "no request should have been sent") 34 | fn.calledOnce.should.equal(true, "callback should have been called once") 35 | fn.thisValues[0].should.equal(visitor, "callback should be called in the context of the visitor instance"); 36 | fn.args[0].should.eql([null, 0], "no error, no requests"); 37 | }); 38 | 39 | it("should include data in POST body", function (done) { 40 | var paramSets = [ 41 | {first: "123"} 42 | ] 43 | 44 | var fn = sinon.spy(function () { 45 | fn.calledOnce.should.equal(true, "callback should have been called once") 46 | fn.thisValues[0].should.equal(visitor, "callback should be called in the context of the visitor instance"); 47 | fn.args[0].should.eql([null, 1], "no error, 1 requests"); 48 | 49 | post.callCount.should.equal(paramSets.length, "each param set should have been POSTed"); 50 | 51 | for (var i = 0; i < paramSets.length; i++) { 52 | var params = paramSets[i]; 53 | var args = post.args[i]; 54 | 55 | var parsedUrl = url.parse(args[0]); 56 | 57 | Math.random(); // I have absolutely no idea why it fails unless there was some processing to be done after url.parse… 58 | 59 | (parsedUrl.protocol + "//" + parsedUrl.host).should.equal(config.hostname); 60 | args[1].should.equal(qs.stringify(params)); 61 | } 62 | 63 | done(); 64 | }); 65 | 66 | var visitor = ua(); 67 | visitor._queue.push.apply(visitor._queue, paramSets); 68 | visitor.send(fn); 69 | }); 70 | 71 | it("should send individual requests when batchting is false", function(done) { 72 | var paramSets = [ 73 | {first: Math.random()}, 74 | {second: Math.random()}, 75 | {third: Math.random()} 76 | ] 77 | 78 | var fn = sinon.spy(function () { 79 | fn.calledOnce.should.equal(true, "callback should have been called once") 80 | fn.thisValues[0].should.equal(visitor, "callback should be called in the context of the visitor instance"); 81 | 82 | fn.args[0].should.eql([null, 3], "no error, 3 requests"); 83 | 84 | done(); 85 | }); 86 | 87 | var visitor = ua({enableBatching:false}); 88 | visitor._queue.push.apply(visitor._queue, paramSets) 89 | visitor.send(fn); 90 | }); 91 | 92 | describe("#batching is true", function() { 93 | it("should send request to collect path when only one payload", function(done) { 94 | var paramSets = [ 95 | {first: Math.random()} 96 | ] 97 | 98 | var fn = sinon.spy(function () { 99 | fn.args[0].should.eql([null, 1], "no error, 1 requests"); 100 | var args = post.args[0]; 101 | 102 | var parsedUrl = url.parse(args[0]); 103 | 104 | parsedUrl.pathname.should.eql(config.path); 105 | done(); 106 | }); 107 | 108 | var visitor = ua({enableBatching:true}); 109 | visitor._queue.push.apply(visitor._queue, paramSets) 110 | visitor.send(fn); 111 | }); 112 | 113 | it("should send request to batch path when more than one payload sent", function(done) { 114 | var paramSets = [ 115 | {first: Math.random()}, 116 | {second: Math.random()}, 117 | {third: Math.random()} 118 | ] 119 | 120 | var fn = sinon.spy(function () { 121 | fn.args[0].should.eql([null, 1], "no error, 1 requests"); 122 | var args = post.args[0]; 123 | 124 | var parsedUrl = url.parse(args[0]); 125 | 126 | parsedUrl.pathname.should.eql(config.batchPath); 127 | done(); 128 | }); 129 | 130 | var visitor = ua({enableBatching:true}); 131 | visitor._queue.push.apply(visitor._queue, paramSets) 132 | visitor.send(fn); 133 | }); 134 | 135 | it("should batch data in Post form", function(done) { 136 | var paramSets = [ 137 | {first: Math.random()}, 138 | {second: Math.random()}, 139 | {third: Math.random()} 140 | ] 141 | 142 | var fn = sinon.spy(function () { 143 | fn.calledOnce.should.equal(true, "callback should have been called once") 144 | fn.thisValues[0].should.equal(visitor, "callback should be called in the context of the visitor instance"); 145 | 146 | fn.args[0].should.eql([null, 1], "no error, 1 requests"); 147 | var args = post.args[0]; 148 | 149 | var params = paramSets; 150 | var formParams = args[1].split("\n"); 151 | formParams.should.have.lengthOf(3); 152 | formParams[0].should.equal(qs.stringify(params[0])); 153 | 154 | done(); 155 | }); 156 | 157 | var visitor = ua({enableBatching:true}); 158 | visitor._queue.push.apply(visitor._queue, paramSets) 159 | visitor.send(fn); 160 | }) 161 | 162 | it("should batch data based on batchSize", function(done) { 163 | var paramSets = [ 164 | {first: Math.random()}, 165 | {second: Math.random()}, 166 | {third: Math.random()} 167 | ] 168 | 169 | var fn = sinon.spy(function () { 170 | fn.calledOnce.should.equal(true, "callback should have been called once") 171 | fn.thisValues[0].should.equal(visitor, "callback should be called in the context of the visitor instance"); 172 | 173 | fn.args[0].should.eql([null, 2], "no error, 2 requests"); 174 | 175 | var body = post.args[0][1]; 176 | 177 | body.split("\n").should.have.lengthOf(2); 178 | 179 | done(); 180 | }); 181 | 182 | var visitor = ua({enableBatching:true, batchSize: 2}); 183 | visitor._queue.push.apply(visitor._queue, paramSets) 184 | visitor.send(fn); 185 | }); 186 | }); 187 | 188 | 189 | 190 | 191 | 192 | 193 | it("should add custom headers to request header", function (done) { 194 | var fn = sinon.spy(function () { 195 | fn.calledOnce.should.equal(true, "callback should have been called once"); 196 | fn.thisValues[0].should.equal(visitor, "callback should be called in the context of the visitor instance"); 197 | 198 | post.calledOnce.should.equal(true, "request should have been POSTed"); 199 | 200 | var parsedUrl = url.parse(post.args[0][0]); 201 | var headers = post.args[0][2]; 202 | 203 | (parsedUrl.protocol + "//" + parsedUrl.host).should.equal(config.hostname); 204 | 205 | headers.should.have.key("User-Agent"); 206 | headers["User-Agent"].should.equal("Test User Agent"); 207 | 208 | done(); 209 | }); 210 | 211 | var visitor = ua({ 212 | headers: {'User-Agent': 'Test User Agent'} 213 | }); 214 | visitor._queue.push({}); 215 | visitor.send(fn); 216 | }); 217 | 218 | 219 | 220 | }) 221 | 222 | }); 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /test/set.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require("querystring"); 3 | var uuid = require("uuid"); 4 | var should = require("should"); 5 | var sinon = require("sinon"); 6 | var url = require("url"); 7 | 8 | var ua = require("../lib/index.js"); 9 | var utils = require("../lib/utils.js"); 10 | var config = require("../lib/config.js"); 11 | var request = require("../lib/request"); 12 | 13 | 14 | describe("ua", function () { 15 | 16 | 17 | 18 | describe("#set", function () { 19 | var _enqueue; 20 | 21 | beforeEach(function () { 22 | _enqueue = sinon.stub(ua.Visitor.prototype, "_enqueue", function () { 23 | if (arguments.length === 3 && typeof arguments[2] === 'function') { 24 | arguments[2](); 25 | } 26 | return this; 27 | }); 28 | }); 29 | 30 | afterEach(function () { 31 | _enqueue.restore() 32 | }); 33 | 34 | 35 | it("should set persistent parameter", function () { 36 | 37 | var visitor = ua("UA-XXXXX-XX") 38 | visitor.set("cd1", "bar") 39 | visitor.pageview("/foo") 40 | 41 | 42 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 43 | _enqueue.args[0][0].should.equal("pageview"); 44 | _enqueue.args[0][1].should.have.keys("dp", "cd1") 45 | 46 | 47 | visitor.pageview("/foo/foo").event("Test Event", "Action") 48 | 49 | _enqueue.args[1][0].should.equal("pageview"); 50 | _enqueue.args[1][1].should.have.keys("dp", "cd1") 51 | 52 | _enqueue.args[2][0].should.equal("event"); 53 | _enqueue.args[2][1].should.have.keys("ec", "ea", "p", "cd1") 54 | }); 55 | 56 | }); 57 | 58 | }); 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /test/timing.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require("querystring"); 3 | var uuid = require("uuid"); 4 | var should = require("should"); 5 | var sinon = require("sinon"); 6 | var url = require("url"); 7 | 8 | var ua = require("../lib/index.js"); 9 | var utils = require("../lib/utils.js"); 10 | var config = require("../lib/config.js"); 11 | var request = require("../lib/request"); 12 | 13 | 14 | describe("ua", function () { 15 | 16 | 17 | describe("#timing", function () { 18 | var _enqueue; 19 | 20 | beforeEach(function () { 21 | _enqueue = sinon.stub(ua.Visitor.prototype, "_enqueue", function () { 22 | if (arguments.length === 3 && typeof arguments[2] === 'function') { 23 | arguments[2](); 24 | } 25 | return this; 26 | }); 27 | }); 28 | 29 | afterEach(function () { 30 | _enqueue.restore() 31 | }); 32 | 33 | 34 | it("should accept arguments (category)", function () { 35 | var category = Math.random().toString(); 36 | 37 | var visitor = ua() 38 | 39 | var result = visitor.timing(category); 40 | 41 | visitor._context = result._context; 42 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 43 | 44 | result.should.be.instanceof(ua.Visitor); 45 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 46 | 47 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 48 | _enqueue.args[0][0].should.equal("timing"); 49 | _enqueue.args[0][1].should.have.keys("utc") 50 | _enqueue.args[0][1].utc.should.equal(category); 51 | }); 52 | 53 | 54 | it("should accept arguments (category, fn)", function () { 55 | var category = Math.random().toString(); 56 | var fn = sinon.spy(); 57 | 58 | var visitor = ua() 59 | 60 | var result = visitor.timing(category, fn); 61 | 62 | visitor._context = result._context; 63 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 64 | 65 | result.should.be.instanceof(ua.Visitor); 66 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 67 | 68 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 69 | _enqueue.args[0][0].should.equal("timing"); 70 | _enqueue.args[0][1].should.have.keys("utc") 71 | _enqueue.args[0][1].utc.should.equal(category); 72 | 73 | fn.calledOnce.should.equal(true, "callback should have been called once") 74 | }); 75 | 76 | 77 | it("should accept arguments (category, variable)", function () { 78 | var category = Math.random().toString(); 79 | var variable = Math.random().toString(); 80 | 81 | var visitor = ua() 82 | 83 | var result = visitor.timing(category, variable); 84 | 85 | visitor._context = result._context; 86 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 87 | 88 | result.should.be.instanceof(ua.Visitor); 89 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 90 | 91 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 92 | _enqueue.args[0][0].should.equal("timing"); 93 | _enqueue.args[0][1].should.have.keys("utc", "utv") 94 | _enqueue.args[0][1].utc.should.equal(category); 95 | _enqueue.args[0][1].utv.should.equal(variable); 96 | }); 97 | 98 | 99 | it("should accept arguments (category, variable, fn)", function () { 100 | var category = Math.random().toString(); 101 | var variable = Math.random().toString(); 102 | var fn = sinon.spy(); 103 | 104 | var visitor = ua() 105 | 106 | var result = visitor.timing(category, variable, fn); 107 | 108 | visitor._context = result._context; 109 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 110 | 111 | result.should.be.instanceof(ua.Visitor); 112 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 113 | 114 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 115 | _enqueue.args[0][0].should.equal("timing"); 116 | _enqueue.args[0][1].should.have.keys("utc", "utv") 117 | _enqueue.args[0][1].utc.should.equal(category); 118 | _enqueue.args[0][1].utv.should.equal(variable); 119 | 120 | fn.calledOnce.should.equal(true, "callback should have been called once") 121 | }); 122 | 123 | 124 | it("should accept arguments (category, variable, time)", function () { 125 | var category = Math.random().toString(); 126 | var variable = Math.random().toString(); 127 | var time = Math.random(); 128 | 129 | var visitor = ua() 130 | 131 | var result = visitor.timing(category, variable, time); 132 | 133 | visitor._context = result._context; 134 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 135 | 136 | result.should.be.instanceof(ua.Visitor); 137 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 138 | 139 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 140 | _enqueue.args[0][0].should.equal("timing"); 141 | _enqueue.args[0][1].should.have.keys("utc", "utv", "utt") 142 | _enqueue.args[0][1].utc.should.equal(category); 143 | _enqueue.args[0][1].utv.should.equal(variable); 144 | _enqueue.args[0][1].utt.should.equal(time); 145 | }); 146 | 147 | 148 | it("should accept arguments (category, variable, time)", function () { 149 | var category = Math.random().toString(); 150 | var variable = Math.random().toString(); 151 | var time = Math.random(); 152 | var fn = sinon.spy() 153 | 154 | var visitor = ua() 155 | 156 | var result = visitor.timing(category, variable, time, fn); 157 | 158 | visitor._context = result._context; 159 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 160 | 161 | result.should.be.instanceof(ua.Visitor); 162 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 163 | 164 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 165 | _enqueue.args[0][0].should.equal("timing"); 166 | _enqueue.args[0][1].should.have.keys("utc", "utv", "utt") 167 | _enqueue.args[0][1].utc.should.equal(category); 168 | _enqueue.args[0][1].utv.should.equal(variable); 169 | _enqueue.args[0][1].utt.should.equal(time); 170 | 171 | fn.calledOnce.should.equal(true, "callback should have been called once") 172 | }); 173 | 174 | 175 | it("should accept arguments (category, variable, time, label)", function () { 176 | var category = Math.random().toString(); 177 | var variable = Math.random().toString(); 178 | var time = Math.random(); 179 | var label = Math.random().toString(); 180 | 181 | var visitor = ua() 182 | 183 | var result = visitor.timing(category, variable, time, label); 184 | 185 | visitor._context = result._context; 186 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 187 | 188 | result.should.be.instanceof(ua.Visitor); 189 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 190 | 191 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 192 | _enqueue.args[0][0].should.equal("timing"); 193 | _enqueue.args[0][1].should.have.keys("utc", "utv", "utt", "utl") 194 | _enqueue.args[0][1].utc.should.equal(category); 195 | _enqueue.args[0][1].utv.should.equal(variable); 196 | _enqueue.args[0][1].utt.should.equal(time); 197 | _enqueue.args[0][1].utl.should.equal(label); 198 | }); 199 | 200 | 201 | it("should accept arguments (category, variable, time, label, fn)", function () { 202 | var category = Math.random().toString(); 203 | var variable = Math.random().toString(); 204 | var time = Math.random(); 205 | var label = Math.random().toString(); 206 | var fn = sinon.spy() 207 | 208 | var visitor = ua() 209 | 210 | var result = visitor.timing(category, variable, time, label, fn); 211 | 212 | visitor._context = result._context; 213 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 214 | 215 | result.should.be.instanceof(ua.Visitor); 216 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 217 | 218 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 219 | _enqueue.args[0][0].should.equal("timing"); 220 | _enqueue.args[0][1].should.have.keys("utc", "utv", "utt", "utl") 221 | _enqueue.args[0][1].utc.should.equal(category); 222 | _enqueue.args[0][1].utv.should.equal(variable); 223 | _enqueue.args[0][1].utt.should.equal(time); 224 | _enqueue.args[0][1].utl.should.equal(label); 225 | 226 | fn.calledOnce.should.equal(true, "callback should have been called once") 227 | }); 228 | 229 | 230 | it("should accept arguments (category, variable, time, label, params)", function () { 231 | var category = Math.random().toString(); 232 | var variable = Math.random().toString(); 233 | var time = Math.random(); 234 | var label = Math.random().toString(); 235 | var params = {p: Math.random().toString()} 236 | 237 | var visitor = ua() 238 | 239 | var result = visitor.timing(category, variable, time, label, params); 240 | 241 | visitor._context = result._context; 242 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 243 | 244 | result.should.be.instanceof(ua.Visitor); 245 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 246 | 247 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 248 | _enqueue.args[0][0].should.equal("timing"); 249 | _enqueue.args[0][1].should.have.keys("utc", "utv", "utt", "utl", "p") 250 | _enqueue.args[0][1].utc.should.equal(category); 251 | _enqueue.args[0][1].utv.should.equal(variable); 252 | _enqueue.args[0][1].utt.should.equal(time); 253 | _enqueue.args[0][1].utl.should.equal(label); 254 | _enqueue.args[0][1].p.should.equal(params.p); 255 | }); 256 | 257 | 258 | it("should accept arguments (category, variable, time, label, params, fn)", function () { 259 | var category = Math.random().toString(); 260 | var variable = Math.random().toString(); 261 | var time = Math.random(); 262 | var label = Math.random().toString(); 263 | var params = {p: Math.random().toString()} 264 | var fn = sinon.spy() 265 | 266 | var visitor = ua() 267 | 268 | var result = visitor.timing(category, variable, time, label, params, fn); 269 | 270 | visitor._context = result._context; 271 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 272 | 273 | result.should.be.instanceof(ua.Visitor); 274 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 275 | 276 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 277 | _enqueue.args[0][0].should.equal("timing"); 278 | _enqueue.args[0][1].should.have.keys("utc", "utv", "utt", "utl", "p") 279 | _enqueue.args[0][1].utc.should.equal(category); 280 | _enqueue.args[0][1].utv.should.equal(variable); 281 | _enqueue.args[0][1].utt.should.equal(time); 282 | _enqueue.args[0][1].utl.should.equal(label); 283 | _enqueue.args[0][1].p.should.equal(params.p); 284 | 285 | fn.calledOnce.should.equal(true, "callback should have been called once") 286 | }); 287 | 288 | 289 | it("should accept arguments (category, variable, time, label, params)", function () { 290 | var params = { 291 | utc: Math.random().toString(), 292 | utv: Math.random().toString(), 293 | utt: Math.random(), 294 | utl: Math.random().toString(), 295 | p: Math.random().toString() 296 | } 297 | 298 | var visitor = ua() 299 | 300 | var result = visitor.timing(params); 301 | 302 | visitor._context = result._context; 303 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 304 | 305 | result.should.be.instanceof(ua.Visitor); 306 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 307 | 308 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 309 | _enqueue.args[0][0].should.equal("timing"); 310 | _enqueue.args[0][1].should.have.keys("utc", "utv", "utt", "utl", "p") 311 | _enqueue.args[0][1].utc.should.equal(params.utc); 312 | _enqueue.args[0][1].utv.should.equal(params.utv); 313 | _enqueue.args[0][1].utt.should.equal(params.utt); 314 | _enqueue.args[0][1].utl.should.equal(params.utl); 315 | _enqueue.args[0][1].p.should.equal(params.p); 316 | }); 317 | 318 | 319 | it("should accept arguments (category, variable, time, label, params)", function () { 320 | var params = { 321 | utc: Math.random().toString(), 322 | utv: Math.random().toString(), 323 | utt: Math.random(), 324 | utl: Math.random().toString(), 325 | p: Math.random().toString() 326 | } 327 | var fn = sinon.spy() 328 | 329 | var visitor = ua() 330 | 331 | var result = visitor.timing(params, fn); 332 | 333 | visitor._context = result._context; 334 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 335 | 336 | result.should.be.instanceof(ua.Visitor); 337 | result._context.should.eql(_enqueue.args[0][1], "the pageview params should be persisted as the context of the visitor clone") 338 | 339 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 340 | _enqueue.args[0][0].should.equal("timing"); 341 | _enqueue.args[0][1].should.have.keys("utc", "utv", "utt", "utl", "p") 342 | _enqueue.args[0][1].utc.should.equal(params.utc); 343 | _enqueue.args[0][1].utv.should.equal(params.utv); 344 | _enqueue.args[0][1].utt.should.equal(params.utt); 345 | _enqueue.args[0][1].utl.should.equal(params.utl); 346 | _enqueue.args[0][1].p.should.equal(params.p); 347 | 348 | fn.calledOnce.should.equal(true, "callback should have been called once") 349 | }); 350 | 351 | }); 352 | 353 | }); 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | -------------------------------------------------------------------------------- /test/transaction.js: -------------------------------------------------------------------------------- 1 | 2 | var qs = require("querystring"); 3 | var uuid = require("uuid"); 4 | var should = require("should"); 5 | var sinon = require("sinon"); 6 | var url = require("url"); 7 | 8 | var ua = require("../lib/index.js"); 9 | var utils = require("../lib/utils.js"); 10 | var config = require("../lib/config.js"); 11 | var request = require("../lib/request"); 12 | 13 | 14 | describe("ua", function () { 15 | 16 | 17 | describe("#transaction", function () { 18 | var _enqueue; 19 | 20 | beforeEach(function () { 21 | _enqueue = sinon.stub(ua.Visitor.prototype, "_enqueue", function (type, params, fn) { 22 | if (fn) { 23 | (typeof fn).should.equal('function', "#_enqueue should receive a callback") 24 | fn(); 25 | } 26 | return this; 27 | }); 28 | }); 29 | 30 | afterEach(function () { 31 | _enqueue.restore() 32 | }); 33 | 34 | 35 | it("should be available via the #t shortcut", function () { 36 | var visitor = ua() 37 | visitor.t.should.equal(visitor.transaction) 38 | }); 39 | 40 | 41 | it("should accept arguments (transaction)", function () { 42 | var transaction = Math.random().toString(); 43 | 44 | var visitor = ua() 45 | 46 | var result = visitor.transaction(transaction); 47 | 48 | visitor._context = result._context; 49 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 50 | 51 | result.should.be.instanceof(ua.Visitor); 52 | result._context.should.eql(_enqueue.args[0][1], "the transaction params should be persisted as the context of the visitor clone") 53 | 54 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 55 | _enqueue.args[0][0].should.equal("transaction"); 56 | _enqueue.args[0][1].should.have.keys("ti") 57 | _enqueue.args[0][1].ti.should.equal(transaction); 58 | }); 59 | 60 | 61 | it("should accept arguments (transaction, fn)", function () { 62 | var transaction = Math.random().toString(); 63 | var fn = sinon.spy() 64 | 65 | ua().transaction(transaction, fn); 66 | 67 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 68 | _enqueue.args[0][0].should.equal("transaction"); 69 | _enqueue.args[0][1].should.have.keys("ti") 70 | _enqueue.args[0][1].ti.should.equal(transaction); 71 | 72 | fn.calledOnce.should.equal(true, "callback should have been called once") 73 | }); 74 | 75 | 76 | it("should accept arguments (transaction, revenue)", function () { 77 | var transaction = Math.random().toString(); 78 | var revenue = Math.random(); 79 | 80 | ua().transaction(transaction, revenue); 81 | 82 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 83 | _enqueue.args[0][0].should.equal("transaction"); 84 | _enqueue.args[0][1].should.have.keys("ti", "tr") 85 | _enqueue.args[0][1].ti.should.equal(transaction); 86 | _enqueue.args[0][1].tr.should.equal(revenue); 87 | }); 88 | 89 | 90 | it("should accept arguments (transaction, revenue, fn)", function () { 91 | var transaction = Math.random().toString(); 92 | var revenue = Math.random(); 93 | var fn = sinon.spy() 94 | 95 | ua().transaction(transaction, revenue, fn); 96 | 97 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 98 | _enqueue.args[0][0].should.equal("transaction"); 99 | _enqueue.args[0][1].should.have.keys("ti", "tr") 100 | _enqueue.args[0][1].ti.should.equal(transaction); 101 | _enqueue.args[0][1].tr.should.equal(revenue); 102 | 103 | fn.calledOnce.should.equal(true, "callback should have been called once") 104 | }); 105 | 106 | 107 | it("should accept arguments (transaction, revenue, shipping)", function () { 108 | var transaction = Math.random().toString(); 109 | var revenue = Math.random(); 110 | var shipping = Math.random(); 111 | 112 | ua().transaction(transaction, revenue, shipping); 113 | 114 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 115 | _enqueue.args[0][0].should.equal("transaction"); 116 | _enqueue.args[0][1].should.have.keys("ti", "tr", "ts") 117 | _enqueue.args[0][1].ti.should.equal(transaction); 118 | _enqueue.args[0][1].tr.should.equal(revenue); 119 | _enqueue.args[0][1].ts.should.equal(shipping); 120 | }); 121 | 122 | 123 | it("should accept arguments (transaction, revenue, shipping, fn)", function () { 124 | var transaction = Math.random().toString(); 125 | var revenue = Math.random(); 126 | var shipping = Math.random(); 127 | var fn = sinon.spy() 128 | 129 | ua().transaction(transaction, revenue, shipping, fn); 130 | 131 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 132 | _enqueue.args[0][0].should.equal("transaction"); 133 | _enqueue.args[0][1].should.have.keys("ti", "tr", "ts") 134 | _enqueue.args[0][1].ti.should.equal(transaction); 135 | _enqueue.args[0][1].tr.should.equal(revenue); 136 | _enqueue.args[0][1].ts.should.equal(shipping); 137 | 138 | fn.calledOnce.should.equal(true, "callback should have been called once") 139 | }); 140 | 141 | 142 | it("should accept arguments (transaction, revenue, shipping, tax)", function () { 143 | var transaction = Math.random().toString(); 144 | var revenue = Math.random(); 145 | var shipping = Math.random(); 146 | var tax = Math.random(); 147 | 148 | ua().transaction(transaction, revenue, shipping, tax); 149 | 150 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 151 | _enqueue.args[0][0].should.equal("transaction"); 152 | _enqueue.args[0][1].should.have.keys("ti", "tr", "ts", "tt") 153 | _enqueue.args[0][1].ti.should.equal(transaction); 154 | _enqueue.args[0][1].tr.should.equal(revenue); 155 | _enqueue.args[0][1].ts.should.equal(shipping); 156 | _enqueue.args[0][1].tt.should.equal(tax); 157 | }); 158 | 159 | 160 | it("should accept arguments (transaction, revenue, shipping, tax, fn)", function () { 161 | var transaction = Math.random().toString(); 162 | var revenue = Math.random(); 163 | var shipping = Math.random(); 164 | var tax = Math.random(); 165 | var fn = sinon.spy() 166 | 167 | ua().transaction(transaction, revenue, shipping, tax, fn); 168 | 169 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 170 | _enqueue.args[0][0].should.equal("transaction"); 171 | _enqueue.args[0][1].should.have.keys("ti", "tr", "ts", "tt") 172 | _enqueue.args[0][1].ti.should.equal(transaction); 173 | _enqueue.args[0][1].tr.should.equal(revenue); 174 | _enqueue.args[0][1].ts.should.equal(shipping); 175 | _enqueue.args[0][1].tt.should.equal(tax); 176 | 177 | fn.calledOnce.should.equal(true, "callback should have been called once") 178 | }); 179 | 180 | 181 | it("should accept arguments (transaction, revenue, shipping, tax, affiliation)", function () { 182 | var transaction = Math.random().toString(); 183 | var revenue = Math.random(); 184 | var shipping = Math.random(); 185 | var tax = Math.random(); 186 | var affiliation = Math.random().toString(); 187 | 188 | ua().transaction(transaction, revenue, shipping, tax, affiliation); 189 | 190 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 191 | _enqueue.args[0][0].should.equal("transaction"); 192 | _enqueue.args[0][1].should.have.keys("ti", "tr", "ts", "tt", "ta") 193 | _enqueue.args[0][1].ti.should.equal(transaction); 194 | _enqueue.args[0][1].tr.should.equal(revenue); 195 | _enqueue.args[0][1].ts.should.equal(shipping); 196 | _enqueue.args[0][1].tt.should.equal(tax); 197 | _enqueue.args[0][1].ta.should.equal(affiliation); 198 | }); 199 | 200 | 201 | it("should accept arguments (transaction, revenue, shipping, tax, affiliation, fn)", function () { 202 | var transaction = Math.random().toString(); 203 | var revenue = Math.random(); 204 | var shipping = Math.random(); 205 | var tax = Math.random(); 206 | var affiliation = Math.random().toString(); 207 | var fn = sinon.spy() 208 | 209 | ua().transaction(transaction, revenue, shipping, tax, affiliation, fn); 210 | 211 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 212 | _enqueue.args[0][0].should.equal("transaction"); 213 | _enqueue.args[0][1].should.have.keys("ti", "tr", "ts", "tt", "ta") 214 | _enqueue.args[0][1].ti.should.equal(transaction); 215 | _enqueue.args[0][1].tr.should.equal(revenue); 216 | _enqueue.args[0][1].ts.should.equal(shipping); 217 | _enqueue.args[0][1].tt.should.equal(tax); 218 | _enqueue.args[0][1].ta.should.equal(affiliation); 219 | 220 | fn.calledOnce.should.equal(true, "callback should have been called once") 221 | }); 222 | 223 | 224 | it("should accept arguments (transaction, revenue, shipping, tax, affiliation, params)", function () { 225 | var transaction = Math.random().toString(); 226 | var revenue = Math.random(); 227 | var shipping = Math.random(); 228 | var tax = Math.random(); 229 | var affiliation = Math.random().toString(); 230 | var params = {p: Math.random().toString()} 231 | 232 | ua().transaction(transaction, revenue, shipping, tax, affiliation, params); 233 | 234 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 235 | _enqueue.args[0][0].should.equal("transaction"); 236 | _enqueue.args[0][1].should.have.keys("ti", "tr", "ts", "tt", "ta", "p") 237 | _enqueue.args[0][1].ti.should.equal(transaction); 238 | _enqueue.args[0][1].tr.should.equal(revenue); 239 | _enqueue.args[0][1].ts.should.equal(shipping); 240 | _enqueue.args[0][1].tt.should.equal(tax); 241 | _enqueue.args[0][1].ta.should.equal(affiliation); 242 | _enqueue.args[0][1].p.should.equal(params.p); 243 | }); 244 | 245 | 246 | it("should accept arguments (transaction, revenue, shipping, tax, affiliation, params, fn)", function () { 247 | var transaction = Math.random().toString(); 248 | var revenue = Math.random(); 249 | var shipping = Math.random(); 250 | var tax = Math.random(); 251 | var affiliation = Math.random().toString(); 252 | var params = {p: Math.random().toString()} 253 | var fn = sinon.spy() 254 | 255 | ua().transaction(transaction, revenue, shipping, tax, affiliation, params, fn); 256 | 257 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 258 | _enqueue.args[0][0].should.equal("transaction"); 259 | _enqueue.args[0][1].should.have.keys("ti", "tr", "ts", "tt", "ta", "p") 260 | _enqueue.args[0][1].ti.should.equal(transaction); 261 | _enqueue.args[0][1].tr.should.equal(revenue); 262 | _enqueue.args[0][1].ts.should.equal(shipping); 263 | _enqueue.args[0][1].tt.should.equal(tax); 264 | _enqueue.args[0][1].ta.should.equal(affiliation); 265 | _enqueue.args[0][1].p.should.equal(params.p); 266 | 267 | fn.calledOnce.should.equal(true, "callback should have been called once") 268 | }); 269 | 270 | 271 | it("should accept arguments (params)", function () { 272 | var params = { 273 | ti: Math.random().toString(), 274 | tr: Math.random(), 275 | ts: Math.random(), 276 | tt: Math.random(), 277 | ta: Math.random().toString(), 278 | p: Math.random().toString(), 279 | empty: null // Should be removed 280 | }; 281 | var json = JSON.stringify(params); 282 | 283 | var visitor = ua() 284 | 285 | var result = visitor.transaction(params); 286 | 287 | visitor._context = result._context; 288 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 289 | 290 | result.should.be.instanceof(ua.Visitor); 291 | result._context.should.eql(_enqueue.args[0][1], "the transaction params should be persisted as the context of the visitor clone") 292 | 293 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 294 | _enqueue.args[0][0].should.equal("transaction"); 295 | _enqueue.args[0][1].should.have.keys("ti", "tr", "ts", "tt", "ta", "p") 296 | _enqueue.args[0][1].ti.should.equal(params.ti); 297 | _enqueue.args[0][1].tr.should.equal(params.tr); 298 | _enqueue.args[0][1].ts.should.equal(params.ts); 299 | _enqueue.args[0][1].tt.should.equal(params.tt); 300 | _enqueue.args[0][1].ta.should.equal(params.ta); 301 | _enqueue.args[0][1].p.should.equal(params.p); 302 | 303 | JSON.stringify(params).should.equal(json, "params should not have changed"); 304 | }); 305 | 306 | 307 | it("should accept arguments (params, fn)", function () { 308 | var params = { 309 | ti: Math.random().toString(), 310 | tr: Math.random(), 311 | ts: Math.random(), 312 | tt: Math.random(), 313 | ta: Math.random().toString(), 314 | p: Math.random().toString() 315 | }; 316 | var fn = sinon.spy() 317 | 318 | var visitor = ua() 319 | 320 | var result = visitor.transaction(params, fn); 321 | 322 | visitor._context = result._context; 323 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 324 | 325 | result.should.be.instanceof(ua.Visitor); 326 | result._context.should.eql(_enqueue.args[0][1], "the transaction params should be persisted as the context of the visitor clone") 327 | 328 | _enqueue.calledOnce.should.equal(true, "#_enqueue should have been called once"); 329 | _enqueue.args[0][0].should.equal("transaction"); 330 | _enqueue.args[0][1].should.have.keys("ti", "tr", "ts", "tt", "ta", "p") 331 | _enqueue.args[0][1].ti.should.equal(params.ti); 332 | _enqueue.args[0][1].tr.should.equal(params.tr); 333 | _enqueue.args[0][1].ts.should.equal(params.ts); 334 | _enqueue.args[0][1].tt.should.equal(params.tt); 335 | _enqueue.args[0][1].ta.should.equal(params.ta); 336 | _enqueue.args[0][1].p.should.equal(params.p); 337 | 338 | fn.calledOnce.should.equal(true, "callback should have been called once") 339 | }); 340 | 341 | it("should fail without transaction ID", function () { 342 | var fn = sinon.spy() 343 | var visitor = ua() 344 | 345 | var result = visitor.transaction(null, fn); 346 | 347 | visitor._context = result._context; 348 | result.should.eql(visitor, "should return a visitor that is identical except for the context"); 349 | 350 | result.should.be.instanceof(ua.Visitor); 351 | result._context.should.eql({}, "the transaction params should not be persisted") 352 | 353 | _enqueue.called.should.equal(false, "#_enqueue should have not been called once"); 354 | fn.calledOnce.should.equal(true, "callback should have been called once"); 355 | fn.args[0][0].should.be.instanceof(Error); 356 | fn.thisValues[0].should.equal(visitor); 357 | }); 358 | 359 | }); 360 | 361 | }); 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | --------------------------------------------------------------------------------