@newrelic/browser-agent 1.232.1 → 1.233.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/dist/cjs/cdn/polyfills.js +5 -2
  2. package/dist/cjs/common/config/state/configurable.js +15 -26
  3. package/dist/cjs/common/config/state/info.js +1 -1
  4. package/dist/cjs/common/config/state/init.js +101 -56
  5. package/dist/cjs/common/config/state/loader-config.js +1 -1
  6. package/dist/cjs/common/config/state/runtime.js +1 -5
  7. package/dist/cjs/common/constants/env.cdn.js +1 -1
  8. package/dist/cjs/common/constants/env.npm.js +1 -1
  9. package/dist/cjs/common/drain/drain.js +1 -1
  10. package/dist/cjs/common/harvest/harvest-scheduler.js +30 -10
  11. package/dist/cjs/common/harvest/harvest.js +119 -55
  12. package/dist/cjs/common/session/session-entity.js +35 -22
  13. package/dist/cjs/common/session/session-entity.test.js +73 -49
  14. package/dist/cjs/common/timer/interaction-timer.js +9 -12
  15. package/dist/cjs/common/url/protocol.test.js +0 -1
  16. package/dist/cjs/common/util/feature-flags.js +2 -1
  17. package/dist/cjs/common/util/submit-data.js +57 -18
  18. package/dist/cjs/common/wrap/wrap-fetch.js +1 -1
  19. package/dist/cjs/common/wrap/wrap-function.js +1 -1
  20. package/dist/cjs/common/wrap/wrap-promise.js +1 -1
  21. package/dist/cjs/features/ajax/aggregate/index.js +2 -2
  22. package/dist/cjs/features/jserrors/aggregate/index.js +7 -5
  23. package/dist/cjs/features/metrics/aggregate/framework-detection.js +67 -0
  24. package/dist/cjs/features/metrics/aggregate/framework-detection.test.js +137 -0
  25. package/dist/cjs/features/metrics/aggregate/index.js +7 -3
  26. package/dist/cjs/features/metrics/aggregate/polyfill-detection.es5.js +14 -0
  27. package/dist/cjs/features/metrics/aggregate/polyfill-detection.es5.test.js +17 -0
  28. package/dist/cjs/features/metrics/aggregate/polyfill-detection.js +53 -0
  29. package/dist/cjs/features/metrics/aggregate/polyfill-detection.test.js +165 -0
  30. package/dist/cjs/features/page_action/aggregate/index.js +2 -2
  31. package/dist/cjs/features/page_view_event/aggregate/index.js +6 -3
  32. package/dist/cjs/features/page_view_timing/aggregate/index.js +2 -2
  33. package/dist/cjs/features/session_replay/aggregate/index.js +333 -0
  34. package/dist/cjs/features/session_replay/constants.js +9 -0
  35. package/dist/cjs/features/session_replay/index.js +12 -0
  36. package/dist/cjs/features/session_replay/instrument/index.js +29 -0
  37. package/dist/cjs/features/session_trace/aggregate/index.js +163 -162
  38. package/dist/cjs/features/session_trace/constants.js +2 -9
  39. package/dist/cjs/features/session_trace/instrument/index.js +24 -66
  40. package/dist/cjs/features/spa/aggregate/index.js +2 -2
  41. package/dist/cjs/features/utils/agent-session.js +1 -2
  42. package/dist/cjs/features/utils/aggregate-base.js +64 -0
  43. package/dist/cjs/features/utils/feature-base.js +0 -31
  44. package/dist/cjs/features/utils/handler-cache.js +3 -4
  45. package/dist/cjs/features/utils/instrument-base.js +42 -10
  46. package/dist/cjs/features/utils/{lazy-loader.js → lazy-feature-loader.js} +4 -2
  47. package/dist/cjs/loaders/agent.js +1 -1
  48. package/dist/cjs/loaders/api/apiAsync.js +3 -1
  49. package/dist/cjs/loaders/configure/configure.js +3 -3
  50. package/dist/cjs/loaders/features/featureDependencies.js +0 -12
  51. package/dist/cjs/loaders/features/features.js +3 -1
  52. package/dist/cjs/loaders/micro-agent.js +6 -6
  53. package/dist/esm/cdn/polyfills.js +5 -2
  54. package/dist/esm/common/config/state/configurable.js +14 -24
  55. package/dist/esm/common/config/state/info.js +2 -2
  56. package/dist/esm/common/config/state/init.js +102 -57
  57. package/dist/esm/common/config/state/loader-config.js +2 -2
  58. package/dist/esm/common/config/state/runtime.js +2 -4
  59. package/dist/esm/common/constants/env.cdn.js +1 -1
  60. package/dist/esm/common/constants/env.npm.js +1 -1
  61. package/dist/esm/common/drain/drain.js +1 -1
  62. package/dist/esm/common/harvest/harvest-scheduler.js +30 -10
  63. package/dist/esm/common/harvest/harvest.js +121 -56
  64. package/dist/esm/common/session/session-entity.js +35 -22
  65. package/dist/esm/common/session/session-entity.test.js +73 -49
  66. package/dist/esm/common/timer/interaction-timer.js +9 -12
  67. package/dist/esm/common/url/protocol.test.js +0 -1
  68. package/dist/esm/common/util/feature-flags.js +2 -1
  69. package/dist/esm/common/util/submit-data.js +57 -18
  70. package/dist/esm/common/wrap/wrap-fetch.js +1 -1
  71. package/dist/esm/common/wrap/wrap-function.js +1 -1
  72. package/dist/esm/common/wrap/wrap-promise.js +1 -1
  73. package/dist/esm/features/ajax/aggregate/index.js +2 -2
  74. package/dist/esm/features/jserrors/aggregate/index.js +7 -5
  75. package/dist/esm/features/metrics/aggregate/framework-detection.js +61 -0
  76. package/dist/esm/features/metrics/aggregate/framework-detection.test.js +133 -0
  77. package/dist/esm/features/metrics/aggregate/index.js +7 -3
  78. package/dist/esm/features/metrics/aggregate/polyfill-detection.es5.js +8 -0
  79. package/dist/esm/features/metrics/aggregate/polyfill-detection.es5.test.js +15 -0
  80. package/dist/esm/features/metrics/aggregate/polyfill-detection.js +47 -0
  81. package/dist/esm/features/metrics/aggregate/polyfill-detection.test.js +163 -0
  82. package/dist/esm/features/page_action/aggregate/index.js +2 -2
  83. package/dist/esm/features/page_view_event/aggregate/index.js +6 -3
  84. package/dist/esm/features/page_view_timing/aggregate/index.js +2 -2
  85. package/dist/esm/features/session_replay/aggregate/index.js +327 -0
  86. package/dist/esm/features/session_replay/constants.js +2 -0
  87. package/dist/esm/features/session_replay/index.js +12 -0
  88. package/dist/esm/features/session_replay/instrument/index.js +21 -0
  89. package/dist/esm/features/session_trace/aggregate/index.js +163 -162
  90. package/dist/esm/features/session_trace/constants.js +1 -5
  91. package/dist/esm/features/session_trace/instrument/index.js +24 -66
  92. package/dist/esm/features/spa/aggregate/index.js +2 -2
  93. package/dist/esm/features/utils/agent-session.js +1 -2
  94. package/dist/esm/features/utils/aggregate-base.js +57 -0
  95. package/dist/esm/features/utils/feature-base.js +1 -32
  96. package/dist/esm/features/utils/handler-cache.js +3 -4
  97. package/dist/esm/features/utils/instrument-base.js +42 -10
  98. package/dist/esm/features/utils/{lazy-loader.js → lazy-feature-loader.js} +3 -1
  99. package/dist/esm/loaders/agent.js +1 -1
  100. package/dist/esm/loaders/api/apiAsync.js +3 -1
  101. package/dist/esm/loaders/configure/configure.js +3 -3
  102. package/dist/esm/loaders/features/featureDependencies.js +0 -11
  103. package/dist/esm/loaders/features/features.js +3 -1
  104. package/dist/esm/loaders/micro-agent.js +6 -6
  105. package/dist/types/common/config/state/configurable.d.ts +1 -3
  106. package/dist/types/common/config/state/configurable.d.ts.map +1 -1
  107. package/dist/types/common/config/state/init.d.ts.map +1 -1
  108. package/dist/types/common/config/state/runtime.d.ts.map +1 -1
  109. package/dist/types/common/harvest/harvest-scheduler.d.ts.map +1 -1
  110. package/dist/types/common/harvest/harvest.d.ts +37 -34
  111. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  112. package/dist/types/common/session/session-entity.d.ts +6 -3
  113. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  114. package/dist/types/common/timer/interaction-timer.d.ts +2 -1
  115. package/dist/types/common/timer/interaction-timer.d.ts.map +1 -1
  116. package/dist/types/common/util/feature-flags.d.ts.map +1 -1
  117. package/dist/types/common/util/submit-data.d.ts +40 -14
  118. package/dist/types/common/util/submit-data.d.ts.map +1 -1
  119. package/dist/types/features/ajax/aggregate/index.d.ts +2 -2
  120. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  121. package/dist/types/features/jserrors/aggregate/index.d.ts +2 -2
  122. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  123. package/dist/types/features/metrics/aggregate/framework-detection.d.ts.map +1 -0
  124. package/dist/types/features/metrics/aggregate/index.d.ts +2 -2
  125. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  126. package/dist/types/features/metrics/aggregate/polyfill-detection.d.ts +6 -0
  127. package/dist/types/features/metrics/aggregate/polyfill-detection.d.ts.map +1 -0
  128. package/dist/types/features/metrics/aggregate/polyfill-detection.es5.d.ts +7 -0
  129. package/dist/types/features/metrics/aggregate/polyfill-detection.es5.d.ts.map +1 -0
  130. package/dist/types/features/page_action/aggregate/index.d.ts +2 -2
  131. package/dist/types/features/page_action/aggregate/index.d.ts.map +1 -1
  132. package/dist/types/features/page_view_event/aggregate/index.d.ts +2 -2
  133. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  134. package/dist/types/features/page_view_timing/aggregate/index.d.ts +2 -2
  135. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  136. package/dist/types/features/session_replay/aggregate/index.d.ts +96 -0
  137. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -0
  138. package/dist/types/features/session_replay/constants.d.ts +2 -0
  139. package/dist/types/features/session_replay/constants.d.ts.map +1 -0
  140. package/dist/types/features/session_replay/index.d.ts +2 -0
  141. package/dist/types/features/session_replay/index.d.ts.map +1 -0
  142. package/dist/types/features/session_replay/instrument/index.d.ts +6 -0
  143. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -0
  144. package/dist/types/features/session_trace/aggregate/index.d.ts +8 -57
  145. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  146. package/dist/types/features/session_trace/constants.d.ts +0 -3
  147. package/dist/types/features/session_trace/constants.d.ts.map +1 -1
  148. package/dist/types/features/session_trace/instrument/index.d.ts +1 -3
  149. package/dist/types/features/session_trace/instrument/index.d.ts.map +1 -1
  150. package/dist/types/features/spa/aggregate/index.d.ts +2 -2
  151. package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
  152. package/dist/types/features/utils/agent-session.d.ts.map +1 -1
  153. package/dist/types/features/utils/aggregate-base.d.ts +11 -0
  154. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -0
  155. package/dist/types/features/utils/feature-base.d.ts +0 -5
  156. package/dist/types/features/utils/feature-base.d.ts.map +1 -1
  157. package/dist/types/features/utils/handler-cache.d.ts.map +1 -1
  158. package/dist/types/features/utils/instrument-base.d.ts +3 -1
  159. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  160. package/dist/types/features/utils/{lazy-loader.d.ts → lazy-feature-loader.d.ts} +2 -2
  161. package/dist/types/features/utils/lazy-feature-loader.d.ts.map +1 -0
  162. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  163. package/dist/types/loaders/features/featureDependencies.d.ts +0 -1
  164. package/dist/types/loaders/features/featureDependencies.d.ts.map +1 -1
  165. package/dist/types/loaders/features/features.d.ts +1 -0
  166. package/dist/types/loaders/features/features.d.ts.map +1 -1
  167. package/package.json +28 -19
  168. package/src/cdn/polyfills.js +4 -1
  169. package/src/common/config/state/configurable.js +18 -24
  170. package/src/common/config/state/info.js +2 -2
  171. package/src/common/config/state/init.js +62 -28
  172. package/src/common/config/state/loader-config.js +2 -2
  173. package/src/common/config/state/runtime.js +2 -4
  174. package/src/common/drain/drain.js +1 -1
  175. package/src/common/harvest/harvest-scheduler.js +35 -10
  176. package/src/common/harvest/harvest.js +73 -50
  177. package/src/common/session/session-entity.js +34 -23
  178. package/src/common/session/session-entity.test.js +57 -51
  179. package/src/common/timer/interaction-timer.js +9 -12
  180. package/src/common/url/protocol.test.js +0 -1
  181. package/src/common/util/feature-flags.js +2 -2
  182. package/src/common/util/submit-data.js +28 -17
  183. package/src/common/wrap/wrap-fetch.js +1 -1
  184. package/src/common/wrap/wrap-function.js +1 -1
  185. package/src/common/wrap/wrap-promise.js +1 -1
  186. package/src/features/ajax/aggregate/index.js +2 -2
  187. package/src/features/jserrors/aggregate/index.js +7 -5
  188. package/src/features/metrics/aggregate/framework-detection.js +73 -0
  189. package/src/features/metrics/aggregate/framework-detection.test.js +201 -0
  190. package/src/features/metrics/aggregate/index.js +8 -3
  191. package/src/features/metrics/aggregate/polyfill-detection.es5.js +9 -0
  192. package/src/features/metrics/aggregate/polyfill-detection.es5.test.js +16 -0
  193. package/src/features/metrics/aggregate/polyfill-detection.js +48 -0
  194. package/src/features/metrics/aggregate/polyfill-detection.test.js +163 -0
  195. package/src/features/page_action/aggregate/index.js +2 -2
  196. package/src/features/page_view_event/aggregate/index.js +5 -5
  197. package/src/features/page_view_timing/aggregate/index.js +2 -2
  198. package/src/features/session_replay/aggregate/index.js +314 -0
  199. package/src/features/session_replay/constants.js +3 -0
  200. package/src/features/session_replay/index.js +12 -0
  201. package/src/features/session_replay/instrument/index.js +22 -0
  202. package/src/features/session_trace/aggregate/index.js +148 -187
  203. package/src/features/session_trace/constants.js +0 -4
  204. package/src/features/session_trace/instrument/index.js +17 -69
  205. package/src/features/spa/aggregate/index.js +2 -2
  206. package/src/features/utils/agent-session.js +1 -2
  207. package/src/features/utils/aggregate-base.js +51 -0
  208. package/src/features/utils/feature-base.js +1 -31
  209. package/src/features/utils/handler-cache.js +3 -4
  210. package/src/features/utils/instrument-base.js +40 -8
  211. package/src/features/utils/{lazy-loader.js → lazy-feature-loader.js} +3 -1
  212. package/src/loaders/agent.js +1 -1
  213. package/src/loaders/api/apiAsync.js +1 -1
  214. package/src/loaders/configure/configure.js +4 -3
  215. package/src/loaders/features/featureDependencies.js +0 -12
  216. package/src/loaders/features/features.js +3 -1
  217. package/src/loaders/micro-agent.js +4 -4
  218. package/dist/cjs/common/metrics/framework-detection.js +0 -72
  219. package/dist/cjs/common/util/user-agent.js +0 -57
  220. package/dist/cjs/common/window/supports-performance-observer.js +0 -15
  221. package/dist/esm/common/metrics/framework-detection.js +0 -66
  222. package/dist/esm/common/util/user-agent.js +0 -48
  223. package/dist/esm/common/window/supports-performance-observer.js +0 -9
  224. package/dist/types/common/metrics/framework-detection.d.ts.map +0 -1
  225. package/dist/types/common/util/user-agent.d.ts +0 -5
  226. package/dist/types/common/util/user-agent.d.ts.map +0 -1
  227. package/dist/types/common/window/supports-performance-observer.d.ts +0 -2
  228. package/dist/types/common/window/supports-performance-observer.d.ts.map +0 -1
  229. package/dist/types/features/utils/lazy-loader.d.ts.map +0 -1
  230. package/src/common/metrics/framework-detection.js +0 -71
  231. package/src/common/util/user-agent.js +0 -56
  232. package/src/common/window/supports-performance-observer.js +0 -10
  233. /package/dist/types/{common/metrics → features/metrics/aggregate}/framework-detection.d.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.232.1",
3
+ "version": "1.233.0",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "Tests for the New Relic JavaScript agent",
@@ -87,16 +87,17 @@
87
87
  "node": ">=12.17.0 < 13.0.0 || >=13.7.0"
88
88
  },
89
89
  "scripts": {
90
- "wdio": "node tools/wdio/bin/cli.js",
90
+ "wdio": "node --max-old-space-size=8192 tools/wdio/bin/cli.js",
91
91
  "start": "npm-run-all --parallel cdn:watch test-server",
92
92
  "test": "jest",
93
93
  "test:watch": "jest --watch",
94
94
  "test:coverage": "jest --coverage",
95
95
  "build:all": "npm run cdn:build:local && npm run build:npm && npm run tools:test-builds",
96
96
  "build:npm": "npm run npm:build:esm && npm run npm:build:cjs && npm run npm:build:types && npm run npm:pack",
97
+ "build:browser-tests": "npm --prefix ./tools/test-builds/browser-tests run build",
97
98
  "lint": "eslint -c .eslintrc.js --ext .js,.cjs,.mjs .",
98
99
  "lint:fix": "npm run lint -- --fix",
99
- "test-all": "tools/jil/bin/cli.js -b *@*",
100
+ "test-jil": "tools/jil/bin/cli.js",
100
101
  "ci-build": "npm ci --cache ./.npm",
101
102
  "build-tar": "npm run ci-build && tar czf ./build/browser-agent.tar.gz --exclude=.git --exclude=.npm --exclude=build/browser-agent.tar.gz .",
102
103
  "watch": "jung -D '\\.npm|.git|node_modules|build|tools|tests' --run -- npm run build:all",
@@ -136,6 +137,8 @@
136
137
  },
137
138
  "dependencies": {
138
139
  "core-js": "^3.26.0",
140
+ "fflate": "^0.7.4",
141
+ "rrweb": "^2.0.0-alpha.8",
139
142
  "web-vitals": "^3.1.0"
140
143
  },
141
144
  "devDependencies": {
@@ -147,18 +150,19 @@
147
150
  "@babel/preset-env": "^7.20.2",
148
151
  "@babel/register": "^7.21.0",
149
152
  "@faker-js/faker": "^7.6.0",
150
- "@fastify/cors": "^8.2.0",
151
- "@fastify/multipart": "^7.3.0",
152
- "@fastify/static": "^6.6.0",
153
+ "@fastify/compress": "^6.2.1",
154
+ "@fastify/cors": "^8.2.1",
155
+ "@fastify/multipart": "^7.6.0",
156
+ "@fastify/static": "^6.10.1",
153
157
  "@newrelic/newrelic-oss-cli": "^0.1.2",
154
158
  "@newrelic/nr-querypack": "https://git@github.com/newrelic/nr-querypack",
155
159
  "@octokit/rest": "^19.0.7",
156
- "@wdio/cli": "^8.1.2",
157
- "@wdio/local-runner": "^8.1.2",
158
- "@wdio/mocha-framework": "^8.1.2",
159
- "@wdio/sauce-service": "^8.1.2",
160
- "@wdio/selenium-standalone-service": "^8.1.2",
161
- "@wdio/spec-reporter": "^8.1.2",
160
+ "@wdio/cli": "^8.10.5",
161
+ "@wdio/local-runner": "^8.10.5",
162
+ "@wdio/mocha-framework": "^8.10.4",
163
+ "@wdio/sauce-service": "^8.10.5",
164
+ "@wdio/selenium-standalone-service": "^8.10.4",
165
+ "@wdio/spec-reporter": "^8.10.5",
162
166
  "aws-sdk": "^2.894.0",
163
167
  "babel-jest": "^29.4.1",
164
168
  "babel-loader": "^8.2.5",
@@ -168,7 +172,7 @@
168
172
  "chalk": "^5.1.2",
169
173
  "charm": "^1.0.1",
170
174
  "concat-stream": "^1.5.1",
171
- "deepmerge-ts": "^4.2.2",
175
+ "deepmerge-ts": "^5.1.0",
172
176
  "defined": "^1.0.0",
173
177
  "dotenv-webpack": "^8.0.1",
174
178
  "eslint": "^8.34.0",
@@ -177,17 +181,18 @@
177
181
  "eslint-plugin-n": "^15.6.1",
178
182
  "eslint-plugin-promise": "^6.1.1",
179
183
  "eslint-plugin-sonarjs": "^0.19.0",
180
- "fastify": "^4.13.0",
181
- "fastify-plugin": "^4.4.0",
182
- "filesize": "^10.0.6",
184
+ "fastify": "^4.17.0",
185
+ "fastify-plugin": "^4.5.0",
186
+ "filesize": "^10.0.7",
183
187
  "fs-extra": "^11.1.0",
184
188
  "function-bind": "^1.1.1",
185
- "glob": "^7.0.5",
189
+ "glob": "^10.2.5",
186
190
  "gzip-size": "^7.0.0",
187
191
  "html-webpack-plugin": "^5.5.0",
188
192
  "husky": "^8.0.0",
189
193
  "jest": "^28.1.1",
190
194
  "jest-environment-jsdom": "28.1.1",
195
+ "jest-extended": "^3.2.4",
191
196
  "jquery": "1.11.3",
192
197
  "jung": "^2.1.0",
193
198
  "just-debounce": "^1.0.0",
@@ -197,13 +202,16 @@
197
202
  "npm-run-all": "^4.1.5",
198
203
  "object-inspect": "^1.5.0",
199
204
  "octokit": "^2.0.14",
205
+ "path-browserify": "^1.0.1",
200
206
  "preprocessify": "0.0.6",
207
+ "process": "^0.11.10",
201
208
  "request": "2.74.0",
202
209
  "sauce-connect-launcher": "^1.2.4",
203
- "saucelabs": "^7.2.0",
210
+ "saucelabs": "^7.2.1",
204
211
  "semver": "^5.3.0",
205
212
  "serialize-anything": "^1.2.3",
206
- "sinon": "^1.17.5",
213
+ "sinon": "^2.4.1",
214
+ "stream-browserify": "^3.0.0",
207
215
  "tap-parser": "^1.2.2",
208
216
  "tape": "^4.8.0",
209
217
  "through": "^2.3.8",
@@ -215,6 +223,7 @@
215
223
  "webpack": "^5.74.0",
216
224
  "webpack-bundle-analyzer": "^4.7.0",
217
225
  "webpack-cli": "^4.10.0",
226
+ "webpack-stream": "^7.0.0",
218
227
  "yargs": "^17.6.2"
219
228
  },
220
229
  "files": [
@@ -5,8 +5,10 @@
5
5
 
6
6
  import 'core-js/stable/promise'
7
7
  import 'core-js/stable/array/includes'
8
- import 'core-js/stable/array/from'
9
8
  import 'core-js/stable/array/find'
9
+ import 'core-js/stable/array/flat'
10
+ import 'core-js/stable/array/flat-map'
11
+ import 'core-js/stable/array/from'
10
12
  import 'core-js/stable/array/some'
11
13
  import 'core-js/stable/object/assign'
12
14
  import 'core-js/stable/object/entries'
@@ -15,3 +17,4 @@ import 'core-js/stable/map'
15
17
  import 'core-js/stable/reflect'
16
18
  import 'core-js/stable/set'
17
19
  import 'core-js/stable/weak-set'
20
+ import 'core-js/stable/object/get-own-property-descriptors'
@@ -1,32 +1,26 @@
1
- import { getFrozenAttributes } from '../../../loaders/features/featureDependencies'
2
1
  import { warn } from '../../util/console'
3
2
 
4
- export class Configurable {
5
- constructor (obj, model) {
6
- Object.assign(this, setValues(obj, model))
7
- }
8
- }
9
-
10
- function setValues (obj, model) {
11
- const state = {}
3
+ export function getModeledObject (obj, model) {
12
4
  try {
13
- if (!obj || typeof obj !== 'object') return warn('New setting a Configurable requires an object as input')
5
+ if (!obj || typeof obj !== 'object') return warn('Setting a Configurable requires an object as input')
14
6
  if (!model || typeof model !== 'object') return warn('Setting a Configurable requires a model to set its initial properties')
15
- Object.assign(state, model)
16
- Object.entries(obj).forEach(([key, value]) => {
17
- if (!Object.keys(model).includes(key)) return
18
- const frozenAttrs = getFrozenAttributes(key)
19
- if (frozenAttrs.length && value && typeof value === 'object') {
20
- frozenAttrs.forEach(attr => {
21
- if (attr in value) {
22
- warn(`"${attr}" is a protected attribute and can not be changed in feature ${key}. It will have no effect.`)
23
- delete value[attr]
24
- }
25
- })
7
+ // allow getters and setters to pass from model to target
8
+ const output = Object.create(
9
+ Object.getPrototypeOf(model),
10
+ Object.getOwnPropertyDescriptors(model)
11
+ )
12
+ const target = Object.keys(output).length === 0 ? obj : output
13
+ for (let key in target) {
14
+ if (obj[key] !== undefined) {
15
+ try {
16
+ if (typeof obj[key] === 'object' && typeof model[key] === 'object') output[key] = getModeledObject(obj[key], model[key])
17
+ else output[key] = obj[key]
18
+ } catch (e) {
19
+ warn('An error occurred while setting a property of a Configurable', e)
20
+ }
26
21
  }
27
- state[key] = value
28
- })
29
- return state
22
+ }
23
+ return output
30
24
  } catch (err) {
31
25
  warn('An error occured while setting a Configurable', err)
32
26
  }
@@ -1,5 +1,5 @@
1
1
  import { defaults as nrDefaults, gosNREUMInitializedAgents } from '../../window/nreum'
2
- import { Configurable } from './configurable'
2
+ import { getModeledObject } from './configurable'
3
3
 
4
4
  const model = {
5
5
  // preset defaults
@@ -44,6 +44,6 @@ export function getInfo (id) {
44
44
 
45
45
  export function setInfo (id, obj) {
46
46
  if (!id) throw new Error('All info objects require an agent identifier!')
47
- _cache[id] = new Configurable(obj, model)
47
+ _cache[id] = getModeledObject(obj, model)
48
48
  gosNREUMInitializedAgents(id, _cache[id], 'info')
49
49
  }
@@ -1,34 +1,68 @@
1
1
 
2
2
  import { DEFAULT_EXPIRES_MS, DEFAULT_INACTIVE_MS } from '../../session/constants'
3
- import { globalScope } from '../../util/global-scope'
4
3
  import { gosNREUMInitializedAgents } from '../../window/nreum'
5
- import { Configurable } from './configurable'
4
+ import { getModeledObject } from './configurable'
6
5
 
7
- const model = {
8
- allow_bfcache: true, // *cli - temporary feature flag for BFCache work
9
- privacy: { cookies_enabled: true }, // *cli - per discussion, default should be true
10
- ajax: { deny_list: undefined, enabled: true, harvestTimeSeconds: 10 },
11
- distributed_tracing: {
12
- enabled: undefined,
13
- exclude_newrelic_header: undefined,
14
- cors_use_newrelic_header: undefined,
15
- cors_use_tracecontext_headers: undefined,
16
- allowed_origins: undefined
17
- },
18
- session: {
19
- domain: undefined, // used by first party cookies to set the top-level domain
20
- expiresMs: DEFAULT_EXPIRES_MS,
21
- inactiveMs: DEFAULT_INACTIVE_MS
22
- },
23
- ssl: undefined,
24
- obfuscate: undefined,
25
- jserrors: { enabled: true, harvestTimeSeconds: 10 },
26
- metrics: { enabled: true },
27
- page_action: { enabled: true, harvestTimeSeconds: 30 },
28
- page_view_event: { enabled: true },
29
- page_view_timing: { enabled: true, harvestTimeSeconds: 30, long_task: false },
30
- session_trace: { enabled: true, harvestTimeSeconds: 10 },
31
- spa: { enabled: true, harvestTimeSeconds: 10 }
6
+ const model = () => {
7
+ const hiddenState = {
8
+ blockSelector: '[data-nr-block]',
9
+ maskInputOptions: { password: true }
10
+ }
11
+ return {
12
+ allow_bfcache: true, // *cli - temporary feature flag for BFCache work
13
+ privacy: { cookies_enabled: true }, // *cli - per discussion, default should be true
14
+ ajax: { deny_list: undefined, enabled: true, harvestTimeSeconds: 10 },
15
+ distributed_tracing: {
16
+ enabled: undefined,
17
+ exclude_newrelic_header: undefined,
18
+ cors_use_newrelic_header: undefined,
19
+ cors_use_tracecontext_headers: undefined,
20
+ allowed_origins: undefined
21
+ },
22
+ session: {
23
+ domain: undefined, // used by first party cookies to set the top-level domain
24
+ expiresMs: DEFAULT_EXPIRES_MS,
25
+ inactiveMs: DEFAULT_INACTIVE_MS
26
+ },
27
+ ssl: undefined,
28
+ obfuscate: undefined,
29
+ jserrors: { enabled: true, harvestTimeSeconds: 10 },
30
+ metrics: { enabled: true },
31
+ page_action: { enabled: true, harvestTimeSeconds: 30 },
32
+ page_view_event: { enabled: true },
33
+ page_view_timing: { enabled: true, harvestTimeSeconds: 30, long_task: false },
34
+ session_trace: { enabled: true, harvestTimeSeconds: 10 },
35
+ session_replay: {
36
+ // feature settings
37
+ enabled: true,
38
+ harvestTimeSeconds: 60,
39
+ sampleRate: 0.1,
40
+ errorSampleRate: 0.1,
41
+ // recording config settings
42
+ maskTextSelector: '*',
43
+ maskAllInputs: true,
44
+ // these properties only have getters because they are enforcable constants and should error if someone tries to override them
45
+ get blockClass () { return 'nr-block' },
46
+ get ignoreClass () { return 'nr-ignore' },
47
+ get maskTextClass () { return 'nr-mask' },
48
+ // props with a getter and setter are used to extend enforcable constants with customer input
49
+ // we must preserve data-nr-block no matter what else the customer sets
50
+ get blockSelector () {
51
+ return hiddenState.blockSelector
52
+ },
53
+ set blockSelector (val) {
54
+ hiddenState.blockSelector += `,${val}`
55
+ },
56
+ // password: must always be present and true no matter what customer sets
57
+ get maskInputOptions () {
58
+ return hiddenState.maskInputOptions
59
+ },
60
+ set maskInputOptions (val) {
61
+ hiddenState.maskInputOptions = { ...val, password: true }
62
+ }
63
+ },
64
+ spa: { enabled: true, harvestTimeSeconds: 10 }
65
+ }
32
66
  }
33
67
 
34
68
  const _cache = {}
@@ -41,7 +75,7 @@ export function getConfiguration (id) {
41
75
 
42
76
  export function setConfiguration (id, obj) {
43
77
  if (!id) throw new Error('All configuration objects require an agent identifier!')
44
- _cache[id] = new Configurable(obj, model)
78
+ _cache[id] = getModeledObject(obj, model())
45
79
  gosNREUMInitializedAgents(id, _cache[id], 'config')
46
80
  }
47
81
 
@@ -1,5 +1,5 @@
1
1
  import { gosNREUMInitializedAgents } from '../../window/nreum'
2
- import { Configurable } from './configurable'
2
+ import { getModeledObject } from './configurable'
3
3
 
4
4
  const model = {
5
5
  accountID: undefined,
@@ -20,6 +20,6 @@ export function getLoaderConfig (id) {
20
20
 
21
21
  export function setLoaderConfig (id, obj) {
22
22
  if (!id) throw new Error('All loader-config objects require an agent identifier!')
23
- _cache[id] = new Configurable(obj, model)
23
+ _cache[id] = getModeledObject(obj, model)
24
24
  gosNREUMInitializedAgents(id, _cache[id], 'loader_config')
25
25
  }
@@ -1,5 +1,4 @@
1
- import * as userAgent from '../../util/user-agent'
2
- import { Configurable } from './configurable'
1
+ import { getModeledObject } from './configurable'
3
2
  import { gosNREUMInitializedAgents } from '../../window/nreum'
4
3
  import { globalScope } from '../../util/global-scope'
5
4
  import { BUILD_ENV, DIST_METHOD, VERSION } from '../../constants/env'
@@ -23,7 +22,6 @@ const model = {
23
22
  releaseIds: {},
24
23
  session: undefined,
25
24
  xhrWrappable: typeof globalScope.XMLHttpRequest?.prototype?.addEventListener === 'function',
26
- userAgent,
27
25
  version: VERSION
28
26
  }
29
27
 
@@ -37,6 +35,6 @@ export function getRuntime (id) {
37
35
 
38
36
  export function setRuntime (id, obj) {
39
37
  if (!id) throw new Error('All runtime objects require an agent identifier!')
40
- _cache[id] = new Configurable(obj, model)
38
+ _cache[id] = getModeledObject(obj, model)
41
39
  gosNREUMInitializedAgents(id, _cache[id], 'runtime')
42
40
  }
@@ -55,7 +55,7 @@ export function drain (agentIdentifier = '', featureName = 'feature') {
55
55
 
56
56
  // Only when the event-groups for all features are ready to drain (staged) do we execute the drain. This has the effect
57
57
  // that the last feature to call drain triggers drain for all features.
58
- const items = Array.from(registry[agentIdentifier])
58
+ const items = [...registry[agentIdentifier]]
59
59
  if (items.every(([key, values]) => values.staged)) {
60
60
  items.sort((a, b) => a[1].priority - b[1].priority)
61
61
  items.forEach(([group]) => {
@@ -68,22 +68,47 @@ export class HarvestScheduler extends SharedContext {
68
68
  if (this.aborted) return
69
69
  var scheduler = this
70
70
 
71
- if (this.opts.getPayload) { // Ajax & PVT
72
- var submitMethod = getSubmitMethod(this.endpoint, opts)
71
+ let harvests = []
72
+ let submitMethod
73
+
74
+ if (this.opts.getPayload) { // Ajax & PVT & SR
75
+ submitMethod = getSubmitMethod(this.endpoint, opts)
73
76
  if (!submitMethod) return false
74
77
 
75
- var retry = submitMethod.method === submitData.xhr
78
+ const retry = submitMethod.method === submitData.xhr
76
79
  var payload = this.opts.getPayload({ retry: retry })
77
- if (payload) {
78
- payload = Object.prototype.toString.call(payload) === '[object Array]' ? payload : [payload]
79
- for (var i = 0; i < payload.length; i++) {
80
- this.harvest.send(this.endpoint, payload[i], opts, submitMethod, onHarvestFinished)
81
- }
82
- }
80
+
81
+ if (!payload) return
82
+
83
+ payload = Object.prototype.toString.call(payload) === '[object Array]' ? payload : [payload]
84
+ harvests.push(...payload)
85
+ }
86
+
87
+ /** sendX is used for features that do not supply a preformatted payload via "getPayload" */
88
+ let send = args => this.harvest.sendX(args)
89
+ if (harvests.length) {
90
+ /** _send is the underlying method for sending in the harvest, if sending raw we can bypass the other helpers completely which format the payloads */
91
+ if (this.opts.raw) send = args => this.harvest._send(args)
92
+ /** send is used to formated the payloads from "getPayload" and obfuscate before sending */
93
+ else send = args => this.harvest.send(args)
83
94
  } else {
84
- this.harvest.sendX(this.endpoint, opts, onHarvestFinished)
95
+ // force it to run at least once in sendX mode
96
+ harvests.push(undefined)
85
97
  }
86
98
 
99
+ harvests.forEach(payload => {
100
+ send({
101
+ endpoint: this.endpoint,
102
+ payload,
103
+ opts,
104
+ submitMethod,
105
+ cbFinished: onHarvestFinished,
106
+ includeBaseParams: this.opts.includeBaseParams,
107
+ customUrl: this.opts.customUrl,
108
+ gzip: this.opts.gzip
109
+ })
110
+ })
111
+
87
112
  if (this.started) {
88
113
  this.scheduleHarvest()
89
114
  }
@@ -8,7 +8,7 @@ import { obj as encodeObj, param as encodeParam } from '../url/encode'
8
8
  import { stringify } from '../util/stringify'
9
9
  import { submitData } from '../util/submit-data'
10
10
  import { getLocation } from '../url/location'
11
- import { getInfo, getConfigurationValue, getRuntime, getConfiguration } from '../config/config'
11
+ import { getInfo, getConfigurationValue, getRuntime } from '../config/config'
12
12
  import { cleanURL } from '../url/clean-url'
13
13
  import { now } from '../timing/now'
14
14
  import { eventListenerOpts } from '../event-listener/event-listener-opts'
@@ -18,6 +18,23 @@ import { SharedContext } from '../context/shared-context'
18
18
  import { VERSION } from '../constants/env'
19
19
  import { isBrowserScope, isWorkerScope } from '../util/global-scope'
20
20
 
21
+ /**
22
+ * @typedef {object} NetworkSendSpec
23
+ * @param {string} endpoint The endpoint to use (jserrors, events, resources etc.)
24
+ * @param {object} payload Object representing payload.
25
+ * @param {object} payload.qs Map of values that should be sent as part of the request query string.
26
+ * @param {string} payload.body String that should be sent as the body of the request.
27
+ * @param {string} payload.body.e Special case of body used for browser interactions.
28
+ * @param {object} opts Additional options for sending data
29
+ * @param {boolean} opts.needResponse Specify whether the caller expects a response data.
30
+ * @param {boolean} opts.unload Specify whether the call is a final harvest during page unload.
31
+ * @param {boolean} opts.sendEmptyBody Specify whether the call should be made even if the body is empty. Useful for rum calls.
32
+ * @param {function} submitMethod The submit method to use {@link ../util/submit-data}
33
+ * @param {string} customUrl Override the beacon url the data is sent to; must include protocol if defined
34
+ * @param {boolean} gzip Enabled gzip compression on the body of the request before it is sent
35
+ * @param {boolean} includeBaseParams Enables the use of base query parameters in the beacon url {@see Harvest.baseQueryString}
36
+ */
37
+
21
38
  const haveSendBeacon = !!navigator.sendBeacon // only the web window obj has sendBeacon at this time, so 'false' for other envs
22
39
 
23
40
  export class Harvest extends SharedContext {
@@ -34,55 +51,48 @@ export class Harvest extends SharedContext {
34
51
  /**
35
52
  * Initiate a harvest from multiple sources. An event that corresponds to the endpoint
36
53
  * name is emitted, which gives any listeners the opportunity to provide payload data.
37
- *
38
- * @param {string} endpoint - The endpoint of the harvest (jserrors, events, resources etc.)
39
- *
40
- * @param {object} opts
41
- * @param {bool} opts.needResponse - Specify whether the caller expects a response data.
42
- * @param {bool} opts.unload - Specify whether the call is a final harvest during page unload.
54
+ * @param {NetworkSendSpec} spec Specification for sending data
43
55
  */
44
- sendX (endpoint, opts, cbFinished) {
56
+ sendX (spec) {
57
+ const { endpoint, opts } = spec
45
58
  var submitMethod = getSubmitMethod(endpoint, opts)
46
59
  if (!submitMethod) return false
47
60
  var options = {
48
61
  retry: submitMethod.method === submitData.xhr
49
62
  }
50
- return this.obfuscator.shouldObfuscate() ? this.obfuscateAndSend(endpoint, this.createPayload(endpoint, options), opts, submitMethod, cbFinished) : this._send(endpoint, this.createPayload(endpoint, options), opts, submitMethod, cbFinished)
63
+ const payload = this.createPayload(endpoint, options)
64
+ var caller = this.obfuscator.shouldObfuscate() ? this.obfuscateAndSend.bind(this) : this._send.bind(this)
65
+ return caller({ ...spec, payload, submitMethod })
51
66
  }
52
67
 
53
68
  /**
54
- * Initiate a harvest call.
55
- *
56
- * @param {string} endpoint - The endpoint of the harvest (jserrors, events, resources etc.)
57
- * @param {object} nr - The loader singleton.
58
- *
59
- * @param {object} singlePayload - Object representing payload.
60
- * @param {object} singlePayload.qs - Map of values that should be sent as part of the request query string.
61
- * @param {string} singlePayload.body - String that should be sent as the body of the request.
62
- * @param {string} singlePayload.body.e - Special case of body used for browser interactions.
63
- *
64
- * @param {object} opts
65
- * @param {bool} opts.needResponse - Specify whether the caller expects a response data.
66
- * @param {bool} opts.unload - Specify whether the call is a final harvest during page unload.
67
- */
68
- send (endpoint, singlePayload, opts, submitMethod, cbFinished) {
69
+ * Initiate a harvest call.
70
+ * @param {NetworkSendSpec} spec Specification for sending data
71
+ */
72
+ send (spec) {
73
+ const { payload = {} } = spec
69
74
  var makeBody = createAccumulator()
70
75
  var makeQueryString = createAccumulator()
71
- if (singlePayload.body) mapOwn(singlePayload.body, makeBody)
72
- if (singlePayload.qs) mapOwn(singlePayload.qs, makeQueryString)
76
+ if (payload.body) mapOwn(payload.body, makeBody)
77
+ if (payload.qs) mapOwn(payload.qs, makeQueryString)
73
78
 
74
- var payload = { body: makeBody(), qs: makeQueryString() }
75
- var caller = this.obfuscator.shouldObfuscate() ? (...args) => this.obfuscateAndSend(...args) : (...args) => this._send(...args)
79
+ var newPayload = { body: makeBody(), qs: makeQueryString() }
80
+ var caller = this.obfuscator.shouldObfuscate() ? this.obfuscateAndSend.bind(this) : this._send.bind(this)
76
81
 
77
- return caller(endpoint, payload, opts, submitMethod, cbFinished)
82
+ return caller({ ...spec, payload: newPayload })
78
83
  }
79
84
 
80
- obfuscateAndSend (endpoint, payload, opts, submitMethod, cbFinished) {
85
+ /**
86
+ * Apply obfuscation rules to the payload and then initial the harvest network call.
87
+ * @param {NetworkSendSpec} spec Specification for sending data
88
+ */
89
+ obfuscateAndSend (spec) {
90
+ const { payload = {} } = spec
81
91
  applyFnToProps(payload, (...args) => this.obfuscator.obfuscateString(...args), 'string', ['e'])
82
- return this._send(endpoint, payload, opts, submitMethod, cbFinished)
92
+ return this._send({ ...spec, payload })
83
93
  }
84
94
 
85
- _send (endpoint, payload, opts, submitMethod, cbFinished) {
95
+ _send ({ endpoint, payload = {}, opts = {}, submitMethod, cbFinished, customUrl, gzip, includeBaseParams = true }) {
86
96
  var info = getInfo(this.sharedContext.agentIdentifier)
87
97
  if (!info.errorBeacon) return false
88
98
 
@@ -95,11 +105,10 @@ export class Harvest extends SharedContext {
95
105
  return false
96
106
  }
97
107
 
98
- if (!opts) opts = {}
99
-
100
- var url = this.getScheme() + '://' + info.errorBeacon + '/' + endpoint + '/1/' + info.licenseKey + this.baseQueryString()
101
- if (payload.qs) url += encodeObj(payload.qs, agentRuntime.maxBytes)
108
+ var url = customUrl || this.getScheme() + '://' + info.errorBeacon + '/' + endpoint + '/1/' + info.licenseKey + '?'
102
109
 
110
+ var baseParams = includeBaseParams ? this.baseQueryString() : ''
111
+ var params = payload.qs ? encodeObj(payload.qs, agentRuntime.maxBytes) : ''
103
112
  if (!submitMethod) {
104
113
  submitMethod = getSubmitMethod(endpoint, opts)
105
114
  }
@@ -107,29 +116,43 @@ export class Harvest extends SharedContext {
107
116
  var useBody = submitMethod.useBody
108
117
 
109
118
  var body
110
- var fullUrl = url
111
- if (useBody && endpoint === 'events') {
112
- body = payload.body.e
113
- } else if (useBody) {
114
- body = stringify(payload.body)
115
- } else {
116
- fullUrl = url + encodeObj(payload.body, agentRuntime.maxBytes)
117
- }
119
+ var fullUrl = url + baseParams + params
120
+
121
+ if (!gzip) {
122
+ if (useBody && endpoint === 'events') {
123
+ body = payload.body.e
124
+ } else if (useBody) {
125
+ body = stringify(payload.body)
126
+ } else {
127
+ fullUrl = fullUrl + encodeObj(payload.body, agentRuntime.maxBytes)
128
+ }
129
+ } else body = payload.body
118
130
 
119
131
  // Get bytes harvested per endpoint as a supportability metric. See metrics aggregator (on unload).
120
132
  agentRuntime.bytesSent[endpoint] = (agentRuntime.bytesSent[endpoint] || 0) + body?.length || 0
121
133
  // Get query bytes harvested per endpoint as a supportability metric. See metrics aggregator (on unload).
122
134
  agentRuntime.queryBytesSent[endpoint] = (agentRuntime.queryBytesSent[endpoint] || 0) + fullUrl.split('?').slice(-1)[0]?.length || 0
123
135
 
136
+ const headers = []
137
+
138
+ if (gzip) {
139
+ headers.push({ key: 'content-type', value: 'application/json' })
140
+ headers.push({ key: 'X-Browser-Monitoring-Key', value: info.licenseKey })
141
+ headers.push({ key: 'Content-Encoding', value: 'gzip' })
142
+ } else {
143
+ headers.push({ key: 'content-type', value: 'text/plain' })
144
+ }
145
+
124
146
  /* Since workers don't support sendBeacon right now, or Image(), they can only use XHR method.
125
147
  Because they still do permit synch XHR, the idea is that at final harvest time (worker is closing),
126
148
  we just make a BLOCKING request--trivial impact--with the remaining data as a temp fill-in for sendBeacon. */
127
- var result = method(fullUrl, body, opts.unload && isWorkerScope)
149
+
150
+ var result = method({ url: fullUrl, body, sync: opts.unload && isWorkerScope, headers })
128
151
 
129
152
  if (cbFinished && method === submitData.xhr) {
130
153
  var xhr = result
131
154
  xhr.addEventListener('load', function () {
132
- var result = { sent: true }
155
+ var result = { sent: true, status: this.status }
133
156
  if (this.status === 429) {
134
157
  result.retry = true
135
158
  result.delay = this.tooManyRequestsDelay
@@ -147,7 +170,7 @@ export class Harvest extends SharedContext {
147
170
  // if beacon request failed, retry with an alternative method -- will not happen for workers
148
171
  if (!result && method === submitData.beacon) {
149
172
  method = submitData.img
150
- result = method(url + encodeObj(payload.body, agentRuntime.maxBytes))
173
+ result = method({ url: fullUrl + encodeObj(payload.body, agentRuntime.maxBytes) })
151
174
  }
152
175
 
153
176
  return result
@@ -162,14 +185,14 @@ export class Harvest extends SharedContext {
162
185
  var ref = this.obfuscator.shouldObfuscate() ? this.obfuscator.obfuscateString(location) : location
163
186
 
164
187
  return ([
165
- '?a=' + info.applicationID,
188
+ 'a=' + info.applicationID,
166
189
  encodeParam('sa', (info.sa ? '' + info.sa : '')),
167
190
  encodeParam('v', VERSION),
168
191
  transactionNameParam(info),
169
192
  encodeParam('ct', runtime.customTransaction),
170
193
  '&rst=' + now(),
171
194
  '&ck=0', // ck param DEPRECATED - still expected by backend
172
- '&s=' + (runtime.session?.value || '0'), // the 0 id encaps all untrackable and default traffic
195
+ '&s=' + (runtime.session?.state.value || '0'), // the 0 id encaps all untrackable and default traffic
173
196
  encodeParam('ref', ref),
174
197
  encodeParam('ptid', (runtime.ptid ? '' + runtime.ptid : ''))
175
198
  ].join(''))
@@ -241,7 +264,7 @@ function createAccumulator () {
241
264
  var accumulator = {}
242
265
  var hasData = false
243
266
  return function (key, val) {
244
- if (val && val.length) {
267
+ if (val !== null && val !== undefined && val.length) {
245
268
  accumulator[key] = val
246
269
  hasData = true
247
270
  }