@zoho_apptics/apptics-js-sdk 2.0.0-beta → 2.0.0-beta.1

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 (220) hide show
  1. package/CHANGELOG.md +111 -5
  2. package/README.md +399 -300
  3. package/dist/apptics.esm.min.js +2 -2
  4. package/dist/apptics.esm.min.js.map +1 -1
  5. package/dist/apptics.min.js +2 -2
  6. package/dist/apptics.min.js.map +1 -1
  7. package/dist/chunks/lifecycle-VYr43KcB.min.js +3 -0
  8. package/dist/chunks/lifecycle-VYr43KcB.min.js.map +1 -0
  9. package/dist/cjs/auto-init.d.ts +3 -3
  10. package/dist/cjs/auto-init.js +1 -1
  11. package/dist/cjs/core/attempts.js +1 -1
  12. package/dist/cjs/core/batch.js +2 -1
  13. package/dist/cjs/core/callsite.d.ts +6 -0
  14. package/dist/cjs/core/callsite.js +25 -3
  15. package/dist/cjs/core/consent.d.ts +5 -1
  16. package/dist/cjs/core/consent.js +8 -6
  17. package/dist/cjs/core/constants.d.ts +35 -10
  18. package/dist/cjs/core/constants.js +31 -15
  19. package/dist/cjs/core/db.d.ts +12 -4
  20. package/dist/cjs/core/db.js +167 -34
  21. package/dist/cjs/core/debug.js +1 -1
  22. package/dist/cjs/core/env.js +1 -1
  23. package/dist/cjs/core/framework-id.d.ts +1 -1
  24. package/dist/cjs/core/framework-id.js +3 -3
  25. package/dist/cjs/core/guard.d.ts +9 -0
  26. package/dist/cjs/core/guard.js +27 -13
  27. package/dist/cjs/core/hooks.d.ts +2 -4
  28. package/dist/cjs/core/hooks.js +2 -3
  29. package/dist/cjs/core/locks.d.ts +9 -0
  30. package/dist/cjs/core/locks.js +22 -0
  31. package/dist/cjs/core/logger.d.ts +3 -4
  32. package/dist/cjs/core/logger.js +4 -40
  33. package/dist/cjs/core/state.d.ts +3 -2
  34. package/dist/cjs/core/state.js +2 -2
  35. package/dist/cjs/core/storage.d.ts +20 -5
  36. package/dist/cjs/core/storage.js +115 -17
  37. package/dist/cjs/core/stylesheet.d.ts +8 -0
  38. package/dist/cjs/core/stylesheet.js +29 -0
  39. package/dist/cjs/core/transport.d.ts +2 -0
  40. package/dist/cjs/core/transport.js +19 -4
  41. package/dist/cjs/core/utils.js +1 -1
  42. package/dist/cjs/core/validate.js +2 -2
  43. package/dist/cjs/device/appversion.d.ts +1 -6
  44. package/dist/cjs/device/appversion.js +13 -1
  45. package/dist/cjs/device/index.d.ts +11 -0
  46. package/dist/cjs/device/index.js +120 -20
  47. package/dist/cjs/engagement/api-rules.d.ts +32 -67
  48. package/dist/cjs/engagement/api-rules.js +144 -157
  49. package/dist/cjs/engagement/auto-api.js +2 -2
  50. package/dist/cjs/engagement/auto-screen.d.ts +2 -0
  51. package/dist/cjs/engagement/auto-screen.js +67 -3
  52. package/dist/cjs/engagement/defined-events.js +1 -1
  53. package/dist/cjs/engagement/send.js +6 -1
  54. package/dist/cjs/engagement/session.d.ts +12 -3
  55. package/dist/cjs/engagement/session.js +161 -25
  56. package/dist/cjs/engagement/track.js +45 -20
  57. package/dist/cjs/errors/send.js +1 -1
  58. package/dist/cjs/errors/track.d.ts +1 -1
  59. package/dist/cjs/errors/track.js +46 -6
  60. package/dist/cjs/feedback/controls.d.ts +6 -0
  61. package/dist/cjs/feedback/controls.js +31 -1
  62. package/dist/cjs/feedback/index.d.ts +8 -7
  63. package/dist/cjs/feedback/index.js +32 -13
  64. package/dist/cjs/feedback/logs.d.ts +10 -5
  65. package/dist/cjs/feedback/logs.js +52 -13
  66. package/dist/cjs/feedback/quartz.d.ts +18 -0
  67. package/dist/cjs/feedback/quartz.js +105 -0
  68. package/dist/cjs/feedback/send.d.ts +3 -0
  69. package/dist/cjs/feedback/send.js +28 -3
  70. package/dist/cjs/feedback/styles.d.ts +10 -0
  71. package/dist/cjs/feedback/styles.js +8 -0
  72. package/dist/cjs/feedback/template.d.ts +1 -1
  73. package/dist/cjs/feedback/template.js +12 -12
  74. package/dist/cjs/feedback/ui.d.ts +14 -6
  75. package/dist/cjs/feedback/ui.js +126 -48
  76. package/dist/cjs/heatmap/send.js +1 -1
  77. package/dist/cjs/heatmap/track.d.ts +1 -1
  78. package/dist/cjs/heatmap/track.js +87 -16
  79. package/dist/cjs/index.d.ts +17 -63
  80. package/dist/cjs/index.js +104 -92
  81. package/dist/cjs/lifecycle.d.ts +8 -1
  82. package/dist/cjs/lifecycle.js +258 -62
  83. package/dist/cjs/remoteconfig/country.d.ts +4 -0
  84. package/dist/cjs/remoteconfig/country.js +73 -0
  85. package/dist/cjs/remoteconfig/custom.d.ts +7 -7
  86. package/dist/cjs/remoteconfig/custom.js +31 -51
  87. package/dist/cjs/remoteconfig/evaluate.d.ts +3 -3
  88. package/dist/cjs/remoteconfig/evaluate.js +56 -43
  89. package/dist/cjs/remoteconfig/fetch.d.ts +3 -3
  90. package/dist/cjs/remoteconfig/fetch.js +9 -23
  91. package/dist/cjs/remoteconfig/index.d.ts +10 -8
  92. package/dist/cjs/remoteconfig/index.js +65 -39
  93. package/dist/cjs/remoteconfig/store.js +1 -1
  94. package/dist/cjs/remoteconfig/tz-countries.d.ts +8 -0
  95. package/dist/cjs/remoteconfig/tz-countries.js +13 -0
  96. package/dist/cjs/remotelog/gate.d.ts +2 -11
  97. package/dist/cjs/remotelog/gate.js +9 -35
  98. package/dist/cjs/remotelog/index.d.ts +4 -21
  99. package/dist/cjs/remotelog/logger.d.ts +11 -17
  100. package/dist/cjs/remotelog/logger.js +30 -58
  101. package/dist/cjs/remotelog/send.js +10 -10
  102. package/dist/cjs/remotelogger/index.d.ts +12 -0
  103. package/dist/cjs/remotelogger/index.js +18 -0
  104. package/dist/cjs/scheduler.js +2 -2
  105. package/dist/cjs/types.d.ts +155 -82
  106. package/dist/cjs/user/index.js +11 -6
  107. package/dist/esm/auto-init.d.ts +3 -3
  108. package/dist/esm/auto-init.js +1 -1
  109. package/dist/esm/core/attempts.js +1 -1
  110. package/dist/esm/core/batch.js +2 -1
  111. package/dist/esm/core/callsite.d.ts +6 -0
  112. package/dist/esm/core/callsite.js +25 -4
  113. package/dist/esm/core/consent.d.ts +5 -1
  114. package/dist/esm/core/consent.js +9 -7
  115. package/dist/esm/core/constants.d.ts +35 -10
  116. package/dist/esm/core/constants.js +30 -15
  117. package/dist/esm/core/db.d.ts +12 -4
  118. package/dist/esm/core/db.js +164 -35
  119. package/dist/esm/core/debug.js +1 -1
  120. package/dist/esm/core/env.js +1 -1
  121. package/dist/esm/core/framework-id.d.ts +1 -1
  122. package/dist/esm/core/framework-id.js +3 -3
  123. package/dist/esm/core/guard.d.ts +9 -0
  124. package/dist/esm/core/guard.js +27 -14
  125. package/dist/esm/core/hooks.d.ts +2 -4
  126. package/dist/esm/core/hooks.js +2 -3
  127. package/dist/esm/core/locks.d.ts +9 -0
  128. package/dist/esm/core/locks.js +20 -0
  129. package/dist/esm/core/logger.d.ts +3 -4
  130. package/dist/esm/core/logger.js +4 -40
  131. package/dist/esm/core/state.d.ts +3 -2
  132. package/dist/esm/core/state.js +2 -2
  133. package/dist/esm/core/storage.d.ts +20 -5
  134. package/dist/esm/core/storage.js +113 -18
  135. package/dist/esm/core/stylesheet.d.ts +8 -0
  136. package/dist/esm/core/stylesheet.js +27 -0
  137. package/dist/esm/core/transport.d.ts +2 -0
  138. package/dist/esm/core/transport.js +19 -5
  139. package/dist/esm/core/utils.js +1 -1
  140. package/dist/esm/core/validate.js +2 -2
  141. package/dist/esm/device/appversion.d.ts +1 -6
  142. package/dist/esm/device/appversion.js +13 -2
  143. package/dist/esm/device/index.d.ts +11 -0
  144. package/dist/esm/device/index.js +123 -24
  145. package/dist/esm/engagement/api-rules.d.ts +32 -67
  146. package/dist/esm/engagement/api-rules.js +143 -155
  147. package/dist/esm/engagement/auto-api.js +3 -3
  148. package/dist/esm/engagement/auto-screen.d.ts +2 -0
  149. package/dist/esm/engagement/auto-screen.js +67 -4
  150. package/dist/esm/engagement/defined-events.js +1 -1
  151. package/dist/esm/engagement/send.js +6 -1
  152. package/dist/esm/engagement/session.d.ts +12 -3
  153. package/dist/esm/engagement/session.js +162 -29
  154. package/dist/esm/engagement/track.js +52 -27
  155. package/dist/esm/errors/send.js +1 -1
  156. package/dist/esm/errors/track.d.ts +1 -1
  157. package/dist/esm/errors/track.js +46 -6
  158. package/dist/esm/feedback/controls.d.ts +6 -0
  159. package/dist/esm/feedback/controls.js +31 -2
  160. package/dist/esm/feedback/index.d.ts +8 -7
  161. package/dist/esm/feedback/index.js +34 -13
  162. package/dist/esm/feedback/logs.d.ts +10 -5
  163. package/dist/esm/feedback/logs.js +54 -15
  164. package/dist/esm/feedback/quartz.d.ts +18 -0
  165. package/dist/esm/feedback/quartz.js +99 -0
  166. package/dist/esm/feedback/send.d.ts +3 -0
  167. package/dist/esm/feedback/send.js +28 -5
  168. package/dist/esm/feedback/styles.d.ts +10 -0
  169. package/dist/esm/feedback/styles.js +4 -0
  170. package/dist/esm/feedback/template.d.ts +1 -1
  171. package/dist/esm/feedback/template.js +12 -12
  172. package/dist/esm/feedback/ui.d.ts +14 -6
  173. package/dist/esm/feedback/ui.js +126 -49
  174. package/dist/esm/heatmap/send.js +1 -1
  175. package/dist/esm/heatmap/track.d.ts +1 -1
  176. package/dist/esm/heatmap/track.js +87 -16
  177. package/dist/esm/index.d.ts +17 -63
  178. package/dist/esm/index.js +108 -80
  179. package/dist/esm/lifecycle.d.ts +8 -1
  180. package/dist/esm/lifecycle.js +266 -71
  181. package/dist/esm/remoteconfig/country.d.ts +4 -0
  182. package/dist/esm/remoteconfig/country.js +70 -0
  183. package/dist/esm/remoteconfig/custom.d.ts +7 -7
  184. package/dist/esm/remoteconfig/custom.js +32 -52
  185. package/dist/esm/remoteconfig/evaluate.d.ts +3 -3
  186. package/dist/esm/remoteconfig/evaluate.js +57 -44
  187. package/dist/esm/remoteconfig/fetch.d.ts +3 -3
  188. package/dist/esm/remoteconfig/fetch.js +9 -23
  189. package/dist/esm/remoteconfig/index.d.ts +10 -8
  190. package/dist/esm/remoteconfig/index.js +66 -40
  191. package/dist/esm/remoteconfig/store.js +1 -1
  192. package/dist/esm/remoteconfig/tz-countries.d.ts +8 -0
  193. package/dist/esm/remoteconfig/tz-countries.js +11 -0
  194. package/dist/esm/remotelog/gate.d.ts +2 -11
  195. package/dist/esm/remotelog/gate.js +10 -32
  196. package/dist/esm/remotelog/index.d.ts +4 -21
  197. package/dist/esm/remotelog/logger.d.ts +11 -17
  198. package/dist/esm/remotelog/logger.js +32 -60
  199. package/dist/esm/remotelog/send.js +12 -12
  200. package/dist/esm/remotelogger/index.d.ts +12 -0
  201. package/dist/esm/remotelogger/index.js +13 -0
  202. package/dist/esm/scheduler.js +2 -2
  203. package/dist/esm/types.d.ts +155 -82
  204. package/dist/esm/user/index.js +12 -6
  205. package/dist/feedback.esm.min.js +2 -2
  206. package/dist/feedback.esm.min.js.map +1 -1
  207. package/package.json +14 -16
  208. package/dist/chunks/batch-BU_mTpm1.min.js +0 -3
  209. package/dist/chunks/batch-BU_mTpm1.min.js.map +0 -1
  210. package/dist/chunks/logs-Bea5zUW_.min.js +0 -3
  211. package/dist/chunks/logs-Bea5zUW_.min.js.map +0 -1
  212. package/dist/cjs/heatmap/index.d.ts +0 -16
  213. package/dist/cjs/heatmap/index.js +0 -22
  214. package/dist/cjs/remotelog/index.js +0 -55
  215. package/dist/esm/heatmap/index.d.ts +0 -16
  216. package/dist/esm/heatmap/index.js +0 -17
  217. package/dist/esm/remotelog/index.js +0 -51
  218. package/dist/heatmap.esm.min.js +0 -3
  219. package/dist/heatmap.esm.min.js.map +0 -1
  220. package/dist/style.css +0 -1
@@ -1,4 +1,4 @@
1
- /*! @zoho_apptics/apptics-js-sdk v2.0.0-beta | (c) Zoho Corporation Private Limited | see LICENSE */
1
+ /*! @zoho_apptics/apptics-js-sdk v2.0.0-beta.1 | (c) Zoho Corporation Private Limited | see LICENSE */
2
2
  'use strict';
3
3
 
4
4
  var constants = require('../core/constants.js');
@@ -8,6 +8,7 @@ var utils = require('../core/utils.js');
8
8
  var storage = require('../core/storage.js');
9
9
  var env = require('../core/env.js');
10
10
  var consent = require('../core/consent.js');
11
+ var locks = require('../core/locks.js');
11
12
 
12
13
  /** The most recently started item of `type` that is still open (endtime 0). */
13
14
  function latestOpen(type, items) {
@@ -58,10 +59,6 @@ function closeScreensOf(session, time) {
58
59
  }, Promise.resolve());
59
60
  });
60
61
  }
61
- /** `track_session: false`, Android's automaticSessionsTrackingStatus: no sessions are recorded. */
62
- function sessionsOff() {
63
- return state.getConfig('track_session') === false; // no i18n
64
- }
65
62
  /**
66
63
  * Track the session.
67
64
  * @param sessionData A set of properties belonging to the session.
@@ -79,13 +76,16 @@ function trackSession(sessionData) {
79
76
  logger.Console.dir(sessionData);
80
77
  return written;
81
78
  }
82
- /** This tab's session when it has one, otherwise the newest open session in storage. */
79
+ /**
80
+ * This tab's session while it is open, otherwise the newest open session in storage: the one the
81
+ * site's other tabs are on, which this tab joins rather than starting a second session beside it.
82
+ */
83
83
  function currentSession() {
84
84
  var mine = tabSessionId();
85
85
  return storage.engagement_transactions.all().then(function (items) {
86
86
  if (mine) {
87
87
  var ours = items.filter(function (item) {
88
- return item.type == constants.AP_CONSTANTS.SESSION && item.id === mine;
88
+ return item.type == constants.AP_CONSTANTS.SESSION && item.id === mine && item.endtime === 0;
89
89
  })[0];
90
90
  if (ours) {
91
91
  return ours;
@@ -94,6 +94,119 @@ function currentSession() {
94
94
  return latestOpen(constants.AP_CONSTANTS.SESSION, items);
95
95
  });
96
96
  }
97
+ /**
98
+ * This page's own id, among the tabs sharing a session. New on every page load, so a reload or a
99
+ * duplicated tab counts as a tab of its own rather than as the one it came from. Made on first use,
100
+ * not when the module loads: getNewUUID reads `screen`, which a server-side render doesn't have.
101
+ */
102
+ var tabIdValue = ''; // no i18n
103
+ function tabId() {
104
+ if (tabIdValue === '') {
105
+ tabIdValue = utils.getNewUUID();
106
+ }
107
+ return tabIdValue;
108
+ }
109
+ /**
110
+ * The tabs of this site using a session. All of them share one session, which ends when the last
111
+ * of them closes (see endCurrentSession). Kept on the stored session, never sent: see
112
+ * sendEngagementData. A session stored by an older SDK has none, and counts as this tab's alone.
113
+ */
114
+ function tabsOf(session) {
115
+ return Array.isArray(session.tabs) ? session.tabs.slice() : [];
116
+ }
117
+ function withThisTab(session) {
118
+ var tabs = tabsOf(session);
119
+ if (tabs.indexOf(tabId()) === -1) {
120
+ tabs.push(tabId());
121
+ }
122
+ return tabs;
123
+ }
124
+ /* ---------------- screen views left open by tabs that are gone ----------------
125
+ * A screen view is ended by the tab that opened it: by the next screen, or as the page hides or
126
+ * closes. A tab that reloads, closes or crashes before that write lands leaves its view open, and
127
+ * nothing ended it until the whole session did, which with tabs sharing one session can be hours.
128
+ *
129
+ * So each tab holds a Web Lock, `apptics-tab-<its id>`, for as long as it is open: the browser
130
+ * lets go of it the moment the tab is gone, however it went. A tab that starts, or comes back into
131
+ * view, ends every open view whose tab no longer holds its lock, at the time that tab was last seen
132
+ * active. Without the Web Locks API, views are left as before and end with their session. */
133
+ const TAB_LOCK_PREFIX = 'apptics-tab-'; // no i18n
134
+ /** A view this young may belong to a tab whose lock is not granted yet: left alone. */
135
+ const YOUNG_VIEW_MS = 5000;
136
+ function tabLocks() {
137
+ var locks = typeof navigator !== 'undefined' ? navigator.locks : undefined; // no i18n
138
+ return locks && typeof locks.request === 'function' && typeof locks.query === 'function' ? locks : undefined; // no i18n
139
+ }
140
+ var holdingTabLock = false;
141
+ /** Takes this tab's lock, held until the page is gone. Once per page. */
142
+ function holdTabLock() {
143
+ var locks = tabLocks();
144
+ if (!locks || holdingTabLock) {
145
+ return;
146
+ }
147
+ holdingTabLock = true;
148
+ locks.request(TAB_LOCK_PREFIX + tabId(), function () {
149
+ return new Promise(function () { });
150
+ }).catch(function () {
151
+ holdingTabLock = false;
152
+ });
153
+ }
154
+ /** Ends the open screen views of tabs that are gone. Resolves right away without Web Locks. */
155
+ function closeScreensOfGoneTabs() {
156
+ var locks = tabLocks();
157
+ if (!locks) {
158
+ return Promise.resolve();
159
+ }
160
+ var mine = tabId();
161
+ return locks.query().then(function (state) {
162
+ var alive = {};
163
+ (state.held || []).concat(state.pending || []).forEach(function (lock) {
164
+ if (lock.name && lock.name.indexOf(TAB_LOCK_PREFIX) === 0) {
165
+ alive[lock.name.slice(TAB_LOCK_PREFIX.length)] = true;
166
+ }
167
+ });
168
+ var now = utils.nowMs();
169
+ return storage.engagement_transactions.all().then(function (items) {
170
+ var gone = items.filter(function (item) {
171
+ return item.type == constants.AP_CONSTANTS.SCREEN && item.endtime === 0 && typeof item.tab === 'string' // no i18n
172
+ && item.tab !== mine && !alive[item.tab] && now - item.starttime > YOUNG_VIEW_MS;
173
+ });
174
+ if (gone.length) {
175
+ logger.Console.log('closeScreensOfGoneTabs: ending ' + gone.length + ' screen view(s) left open by a tab that is gone'); // no i18n
176
+ }
177
+ return gone.reduce(function (done, item) {
178
+ // when that tab was last seen with the view open, not now: the time since is not time spent
179
+ var end = Math.max(item.starttime, Number(item.lastseen) || item.starttime);
180
+ return done.then(function () {
181
+ return storage.engagement_transactions.update(Object.assign({}, item, { endtime: end }));
182
+ });
183
+ }, Promise.resolve());
184
+ });
185
+ }).catch(function (err) {
186
+ logger.Console.log('closeScreensOfGoneTabs: ' + err); // no i18n
187
+ });
188
+ }
189
+ /** Notes on this tab's open view that the tab is still here, the end a gone tab's view is given. */
190
+ function markScreenSeen(time) {
191
+ var current = state._ZAP._current_screen;
192
+ if (!current || !current.id || current.endtime) {
193
+ return Promise.resolve();
194
+ }
195
+ return storage.engagement_transactions.all().then(function (items) {
196
+ var open = items.filter(function (item) {
197
+ return item.type == constants.AP_CONSTANTS.SCREEN && item.id === current.id && item.endtime === 0;
198
+ })[0];
199
+ return open ? storage.engagement_transactions.update(Object.assign({}, open, { lastseen: time })) : undefined;
200
+ });
201
+ }
202
+ /**
203
+ * Every change to the session runs under a lock shared by the site's tabs: each one reads the
204
+ * stored session, changes it and writes it back, and two tabs doing that at once would each write
205
+ * over the other's change.
206
+ */
207
+ function withSessionLock(run) {
208
+ return locks.withLock('session', run); // no i18n
209
+ }
97
210
  /** The id of this tab's session. sessionStorage is per tab and survives a reload. */
98
211
  const TAB_KEY = 'apptics-session'; // no i18n
99
212
  function tabSessionId() {
@@ -112,9 +225,9 @@ function rememberTabSession(id) {
112
225
  // the session still works, it just can't be recognised after a reload
113
226
  }
114
227
  }
115
- /** True until the first session on this device has been recorded. */
228
+ /** True until the first session on this device has been recorded: device_info's hadsession. */
116
229
  function isFirstEverSession() {
117
- return utils.isEmpty(storage.user_defaults.item(HAD_SESSION_KEY));
230
+ return storage.getKeyFromDeviceInfo(HAD_SESSION_KEY) !== true;
118
231
  }
119
232
  const HAD_SESSION_KEY = 'hadsession'; // no i18n
120
233
  /**
@@ -151,7 +264,7 @@ function commitPendingSession() {
151
264
  // the flag is set here rather than at the start, so a dropped session does not consume it and
152
265
  // leave the next real session reporting firstsession false
153
266
  if (session.firstsession) {
154
- storage.user_defaults.add(HAD_SESSION_KEY, true);
267
+ storage.setDeviceInfoKey(HAD_SESSION_KEY, true);
155
268
  }
156
269
  return trackSession(session);
157
270
  }
@@ -176,6 +289,7 @@ function startNewSession(time) {
176
289
  firstsession: isFirstEverSession(),
177
290
  networkstatus: env.getNetworkStatus(),
178
291
  lastactivetime: time,
292
+ tabs: [tabId()],
179
293
  };
180
294
  // ram is optional in sessionWebSDK but checked against ram_rom ('4 GB'), which an empty
181
295
  // string cannot match. navigator.deviceMemory is Chromium-only, so Firefox and Safari have
@@ -202,13 +316,14 @@ function startNewSession(time) {
202
316
  /** The un-deferred path, for a minimum of 0. */
203
317
  function commitNow(session) {
204
318
  if (session.firstsession) {
205
- storage.user_defaults.add(HAD_SESSION_KEY, true);
319
+ storage.setDeviceInfoKey(HAD_SESSION_KEY, true);
206
320
  }
207
321
  return trackSession(session);
208
322
  }
209
323
  /**
210
- * Closes this tab's session, so it can be sent: the tab is closing or the page is being replaced.
211
- * Nothing happens when the session is already closed.
324
+ * This tab is closing, or the page is being replaced. The session ends here when this is the last
325
+ * tab using it, so it can be sent; while another tab of the site is still on it, this tab only
326
+ * leaves it, and the last tab to close ends it. Nothing happens when it is already closed.
212
327
  */
213
328
  function endCurrentSession() {
214
329
  var time = utils.nowMs();
@@ -216,11 +331,23 @@ function endCurrentSession() {
216
331
  if (dropPendingSession()) {
217
332
  return Promise.resolve();
218
333
  }
334
+ return withSessionLock(function () {
335
+ return closeOrLeaveSession(time);
336
+ });
337
+ }
338
+ function closeOrLeaveSession(time) {
219
339
  return currentSession().then(function (_currentSession) {
220
340
  if (!_currentSession || _currentSession.endtime !== 0) {
221
341
  return;
222
342
  }
223
- var closed = Object.assign({}, _currentSession, { endtime: time, lastactivetime: time });
343
+ var others = tabsOf(_currentSession).filter(function (tab) { return tab !== tabId(); });
344
+ if (others.length > 0) {
345
+ var left = Object.assign({}, _currentSession, { tabs: others, lastactivetime: time });
346
+ state._ZAP._current_session = left;
347
+ logger.Console.log('endCurrentSession: ' + others.length + ' other tab(s) still on this session, so it stays open'); // no i18n
348
+ return storage.engagement_transactions.update(left);
349
+ }
350
+ var closed = Object.assign({}, _currentSession, { endtime: time, lastactivetime: time, tabs: [] });
224
351
  state._ZAP._current_session = closed;
225
352
  logger.Console.log('endCurrentSession: closing the session opened at ' + closed.starttime); // no i18n
226
353
  return storage.engagement_transactions.update(closed).then(function () {
@@ -257,18 +384,21 @@ function closeAbandonedSessions() {
257
384
  }
258
385
  function checkAndUpdateSession() {
259
386
  if (!state._ZAP._token) {
260
- logger.logFailure(constants.AP_LOG_MODULE.SESSION, 'checkAndUpdateSession: You must call apptics.init() first'); // no i18n
387
+ logger.logFailure(constants.AP_LOG_MODULE.SESSION, 'checkAndUpdateSession: You must call Apptics.init() first'); // no i18n
261
388
  return Promise.resolve();
262
389
  }
263
390
  if (env.is_apptics_disabled()) {
264
391
  return Promise.resolve();
265
392
  }
266
- if (sessionsOff()) {
267
- return Promise.resolve();
268
- }
269
393
  var time = utils.nowMs();
270
- // a tab that was killed before it could close its session: close it so it gets sent
271
- return closeAbandonedSessions().then(currentSession).then(function (_currentSession) {
394
+ return withSessionLock(function () {
395
+ return settleSession(time);
396
+ });
397
+ }
398
+ function settleSession(time) {
399
+ // a tab that was killed before it could close its session: close it so it gets sent; and the
400
+ // screen views left open by tabs that are gone, so they end now rather than with the session
401
+ return closeAbandonedSessions().then(closeScreensOfGoneTabs).then(currentSession).then(function (_currentSession) {
272
402
  // a closed session belongs to a tab that has gone: this tab starts its own
273
403
  if (!_currentSession || _currentSession.endtime !== 0) {
274
404
  return startNewSession(time);
@@ -289,13 +419,14 @@ function checkAndUpdateSession() {
289
419
  });
290
420
  }
291
421
  logger.Console.log('checkAndUpdateSession: User is back before ' + state._ZAP.inactiveduration + ' minutes so updating the lastactivitytime - ' + time); // no i18n
292
- return updateLastActiveTimeOfSession();
422
+ return touchSession();
293
423
  });
294
424
  }
295
425
  function updateLastActiveTimeOfSession() {
296
- if (sessionsOff()) {
297
- return Promise.resolve();
298
- }
426
+ return withSessionLock(touchSession);
427
+ }
428
+ /** Marks the session active now, with this tab among its tabs. Callers hold the session lock. */
429
+ function touchSession() {
299
430
  var time = utils.nowMs();
300
431
  // a pending session is not in the store yet, so it is updated in place instead
301
432
  if (pendingSession) {
@@ -308,20 +439,25 @@ function updateLastActiveTimeOfSession() {
308
439
  return;
309
440
  }
310
441
  logger.Console.log('updateLastActiveTimeOfSession: Updating the lastactivitytime - ' + time); // no i18n
311
- var _newSession = Object.assign({}, _currentSession, { lastactivetime: time });
442
+ var _newSession = Object.assign({}, _currentSession, { lastactivetime: time, tabs: withThisTab(_currentSession) });
312
443
  state._ZAP._current_session = _newSession;
313
444
  return storage.engagement_transactions.update(_newSession);
445
+ }).then(function () {
446
+ return markScreenSeen(time);
314
447
  });
315
448
  }
316
449
 
317
450
  exports.checkAndUpdateSession = checkAndUpdateSession;
318
451
  exports.closeAbandonedSessions = closeAbandonedSessions;
319
452
  exports.closeScreen = closeScreen;
453
+ exports.closeScreensOfGoneTabs = closeScreensOfGoneTabs;
320
454
  exports.commitPendingSession = commitPendingSession;
321
455
  exports.currentSession = currentSession;
322
456
  exports.dropPendingSession = dropPendingSession;
323
457
  exports.endCurrentSession = endCurrentSession;
458
+ exports.holdTabLock = holdTabLock;
324
459
  exports.startNewSession = startNewSession;
460
+ exports.tabId = tabId;
325
461
  exports.trackSession = trackSession;
326
462
  exports.updateLastActiveTimeOfSession = updateLastActiveTimeOfSession;
327
463
  exports.updateScreenWithEndtime = updateScreenWithEndtime;
@@ -1,4 +1,4 @@
1
- /*! @zoho_apptics/apptics-js-sdk v2.0.0-beta | (c) Zoho Corporation Private Limited | see LICENSE */
1
+ /*! @zoho_apptics/apptics-js-sdk v2.0.0-beta.1 | (c) Zoho Corporation Private Limited | see LICENSE */
2
2
  'use strict';
3
3
 
4
4
  var constants = require('../core/constants.js');
@@ -16,6 +16,20 @@ var apiRules = require('./api-rules.js');
16
16
  var definedEvents = require('./defined-events.js');
17
17
  var autoScreen = require('./auto-screen.js');
18
18
 
19
+ /** Messages already printed, so a name refused in a loop says so once. */
20
+ var refusedSaid = {};
21
+ /**
22
+ * A call the SDK refused because of what the app passed it (an invalid event, group or screen
23
+ * name): reported as every failure is, and said in the console too, debug or not, once per
24
+ * message. Nothing else tells an integrator why the event or screen never shows up.
25
+ */
26
+ function refused(module, message) {
27
+ logger.logFailure(module, message);
28
+ if (!refusedSaid[message]) {
29
+ refusedSaid[message] = true;
30
+ logger.warnAlways(message);
31
+ }
32
+ }
19
33
  /**
20
34
  * Records an event: `trackEvent(name, group?, properties?)`, or a defined event with
21
35
  * `trackEvent(DefinedEvents.AP_USER_LOGIN, properties?)`, which brings its own group.
@@ -37,14 +51,14 @@ function trackEvent(eventName, groupName, jsonObj) {
37
51
  }
38
52
  else {
39
53
  if (typeof eventName !== 'string' || !validate.validateEventOrGroup(eventName)) { // no i18n
40
- logger.logFailure(constants.AP_LOG_MODULE.EVENT, 'AppticsEvents.addEvent: Invalid eventName: ' + String(eventName)); // no i18n
54
+ refused(constants.AP_LOG_MODULE.EVENT, 'AppticsEvent.addEvent: Invalid eventName: ' + String(eventName)); // no i18n
41
55
  return;
42
56
  }
43
57
  if (groupName === undefined || groupName === null || groupName === '') { // no i18n
44
58
  groupName = constants.AP_CONFIG.default_group_name;
45
59
  }
46
60
  else if (typeof groupName !== 'string' || !validate.validateEventOrGroup(groupName)) { // no i18n
47
- logger.logFailure(constants.AP_LOG_MODULE.EVENT, 'AppticsEvents.addEvent: Invalid groupName for "' + eventName + '": ' + String(groupName)); // no i18n
61
+ refused(constants.AP_LOG_MODULE.EVENT, 'AppticsEvent.addEvent: Invalid groupName for "' + eventName + '": ' + String(groupName)); // no i18n
48
62
  return;
49
63
  }
50
64
  }
@@ -70,7 +84,7 @@ function trackEvent(eventName, groupName, jsonObj) {
70
84
  session.commitPendingSession();
71
85
  debug.noteDebugActivity();
72
86
  storage.engagement_transactions.add(eventData);
73
- logger.Console.log('AppticsEvents.addEvent(\'' + eventName + '\',\'' + groupName + '\',' + JSON.stringify(_jsonObj || {}) + ')'); // no i18n
87
+ logger.Console.log('AppticsEvent.addEvent(\'' + eventName + '\',\'' + groupName + '\',' + JSON.stringify(_jsonObj || {}) + ')'); // no i18n
74
88
  logger.Console.dir(eventData);
75
89
  }
76
90
  /**
@@ -80,7 +94,7 @@ function trackEvent(eventName, groupName, jsonObj) {
80
94
  */
81
95
  function inScreenAt(screenName, jsonObj, id, startedAt) {
82
96
  if (!validate.validateScreenName(screenName)) {
83
- logger.logFailure(constants.AP_LOG_MODULE.SCREEN, 'AppticsScreenTracker.inScreen: Invalid screenName: ' + String(screenName)); // no i18n
97
+ refused(constants.AP_LOG_MODULE.SCREEN, 'AppticsScreen.inScreen: Invalid screenName: ' + String(screenName)); // no i18n
84
98
  return;
85
99
  }
86
100
  // the app named this screen itself: the automatic view of the same navigation gives way to it
@@ -90,7 +104,7 @@ function inScreenAt(screenName, jsonObj, id, startedAt) {
90
104
  /** Ends the screen view inScreen() began, as Android's AppticsScreenTracker.outScreen(id). */
91
105
  function outScreenAt(id, endedAt) {
92
106
  if (utils.isEmpty(id)) {
93
- logger.logFailure(constants.AP_LOG_MODULE.SCREEN, 'AppticsScreenTracker.outScreen: no screen id given'); // no i18n
107
+ logger.logFailure(constants.AP_LOG_MODULE.SCREEN, 'AppticsScreen.outScreen: no screen id given'); // no i18n
94
108
  return;
95
109
  }
96
110
  session.closeScreen(id, endedAt);
@@ -107,7 +121,7 @@ function recordScreen(screenName, jsonObj, id, startedAt, implicit) {
107
121
  return;
108
122
  }
109
123
  if (!validate.validateScreenName(screenName)) {
110
- logger.logFailure(constants.AP_LOG_MODULE.SCREEN, 'AppticsScreenTracker.inScreen: Invalid screenName: ' + String(screenName)); // no i18n
124
+ refused(constants.AP_LOG_MODULE.SCREEN, 'AppticsScreen.inScreen: Invalid screenName: ' + String(screenName)); // no i18n
111
125
  return;
112
126
  }
113
127
  jsonObj = jsonObj || {};
@@ -124,7 +138,10 @@ function recordScreen(screenName, jsonObj, id, startedAt, implicit) {
124
138
  networkstatus: env.getNetworkStatus(),
125
139
  orientation: env.getOrientation(),
126
140
  sessionstarttime: state._ZAP._current_session.starttime || time,
127
- customproperties: _jsonObj
141
+ customproperties: _jsonObj,
142
+ // the tab showing it, so a tab that is gone can have its view ended (closeScreensOfGoneTabs).
143
+ // SDK bookkeeping, removed before sending, as lastseen is
144
+ tab: session.tabId(),
128
145
  };
129
146
  // the screen being left ends where this one starts
130
147
  var previous = state._ZAP._current_screen && state._ZAP._current_screen.id;
@@ -137,7 +154,7 @@ function recordScreen(screenName, jsonObj, id, startedAt, implicit) {
137
154
  // `implicit`: a view recorded only so a heatmap record has one to belong to. Kept on the
138
155
  // in-memory copy alone, never on the stored item, which goes to the server as it is
139
156
  state._ZAP._current_screen = implicit ? Object.assign({}, screenData, { implicit: true }) : screenData;
140
- logger.Console.log('AppticsScreenTracker.inScreen(\'' + screenName + '\',' + JSON.stringify(_jsonObj || {}) + ')'); // no i18n
157
+ logger.Console.log('AppticsScreen.inScreen(\'' + screenName + '\',' + JSON.stringify(_jsonObj || {}) + ')'); // no i18n
141
158
  logger.Console.dir(screenData);
142
159
  }
143
160
  /**
@@ -226,16 +243,19 @@ function trackApi(jsonObj) {
226
243
  var hostname = jsonObj.hostname || '';
227
244
  var port = (typeof jsonObj.port === 'number') ? jsonObj.port : 0;
228
245
  var path = jsonObj.path || '';
246
+ // a call reported by its parts, a hostname and a path with no url: its scheme and port are its
247
+ // own (https and 443 unless given), not the page's, which belong to another host
248
+ var partsOnly = utils.isEmpty(jsonObj.url) && !utils.isEmpty(jsonObj.hostname);
229
249
  if (!utils.isEmpty(rawUrl)) {
230
250
  try {
231
251
  var urlObj = new URL(rawUrl, window.location.origin);
232
- if (utils.isEmpty(scheme)) {
252
+ if (utils.isEmpty(scheme) && !partsOnly) {
233
253
  scheme = urlObj.protocol.replace(':', ''); // "https:"
234
254
  }
235
255
  if (utils.isEmpty(hostname)) {
236
256
  hostname = urlObj.hostname;
237
257
  }
238
- if (!port) {
258
+ if (!port && !partsOnly) {
239
259
  if (urlObj.port) {
240
260
  port = parseInt(urlObj.port, 10);
241
261
  }
@@ -256,22 +276,27 @@ function trackApi(jsonObj) {
256
276
  }
257
277
  // only http and https exist as far as the proxy's api_scheme is concerned
258
278
  scheme = (scheme === 'https' || scheme === 'http') ? scheme : 'https'; // no i18n
279
+ if (!port) {
280
+ port = scheme === 'https' ? 443 : 80; // no i18n
281
+ }
259
282
  // a hostname the proxy refuses cannot be repaired without inventing one, so the call is
260
283
  // dropped here and said out loud. localhost and bare IPs land here: api_hostname wants at
261
284
  // least one dot and an alphabetic suffix, which is worth knowing when nothing shows up locally
262
285
  if (!API_HOSTNAME.test(hostname)) {
263
- logger.logFailure(constants.AP_LOG_MODULE.API, 'AppticsApiTracker: hostname not accepted by the server: ' + String(hostname)); // no i18n
286
+ logger.logFailure(constants.AP_LOG_MODULE.API, 'AppticsApi: hostname not accepted by the server: ' + String(hostname)); // no i18n
264
287
  return;
265
288
  }
266
- // configureApiTracking: the app's rules decide whether this call is recorded at all
267
- if (!apiRules.hostAllowed(hostname) || apiRules.endpointIgnored(path)) {
268
- logger.Console.log('AppticsApiTracker: ' + hostname + withoutQueryForLog(path) + ' is left out by configureApiTracking'); // no i18n
289
+ // configureApiTracking: the app's rules decide whether this call is recorded at all, tried on
290
+ // the URL as it was called, without its query
291
+ var defaultPort = (scheme === 'https' && port === 443) || (scheme === 'http' && port === 80); // no i18n
292
+ var calledUrl = scheme + '://' + hostname + (port && !defaultPort ? ':' + port : '') + (String(path).charAt(0) === '/' ? '' : '/') + path; // no i18n
293
+ if (!apiRules.urlTracked(calledUrl)) {
294
+ logger.Console.log('AppticsApi: ' + hostname + withoutQueryForLog(path) + ' is left out by configureApiTracking'); // no i18n
269
295
  return;
270
296
  }
271
- // and the name it is recorded under: ids become *, regional hosts one group, so the console
272
- // groups calls by endpoint rather than by every id that went through it
297
+ // and the name it is recorded under: ids become *, so the console groups calls by endpoint
298
+ // rather than by every id that went through it
273
299
  path = sanitizeApiPath(apiRules.normalizeApiPath(path));
274
- hostname = apiRules.groupedHost(hostname);
275
300
  var sessionStart = state._ZAP._current_session.starttime || starttime;
276
301
  // no customproperties: apiWebSDK does not declare the key, unlike the screen and event
277
302
  // templates, so anything extra the caller passes is context for the log line only
@@ -296,7 +321,7 @@ function trackApi(jsonObj) {
296
321
  session.commitPendingSession();
297
322
  debug.noteDebugActivity();
298
323
  storage.engagement_transactions.add(apiData);
299
- logger.Console.log('AppticsApiTracker(\'' + method + ' ' + scheme + '://' + hostname + path + '\')'); // no i18n
324
+ logger.Console.log('AppticsApi(\'' + method + ' ' + scheme + '://' + hostname + path + '\')'); // no i18n
300
325
  logger.Console.dir(apiData);
301
326
  }
302
327
  /** Begun calls by id. Bounded, so an app that starts calls it never ends can't grow it forever. */
@@ -324,7 +349,7 @@ function startTrackApi(url, method) {
324
349
  function endTrackApiAt(id, responseCode, responseMessage, endedAt) {
325
350
  var open = openCalls[id];
326
351
  if (!open) {
327
- logger.logFailure(constants.AP_LOG_MODULE.API, 'AppticsApiTracker.endTrackApi: no call started with id ' + String(id)); // no i18n
352
+ logger.logFailure(constants.AP_LOG_MODULE.API, 'AppticsApi.endTrackApi: no call started with id ' + String(id)); // no i18n
328
353
  return;
329
354
  }
330
355
  delete openCalls[id];
@@ -1,4 +1,4 @@
1
- /*! @zoho_apptics/apptics-js-sdk v2.0.0-beta | (c) Zoho Corporation Private Limited | see LICENSE */
1
+ /*! @zoho_apptics/apptics-js-sdk v2.0.0-beta.1 | (c) Zoho Corporation Private Limited | see LICENSE */
2
2
  'use strict';
3
3
 
4
4
  var state = require('../core/state.js');
@@ -1,4 +1,4 @@
1
1
  import type { Properties } from '../types.js';
2
2
  export declare function trackError(errorEvent: any, jsonObj?: Properties): void;
3
- export declare function enableGlobalErrorHandler(status?: boolean): void;
3
+ export declare function trackGlobalErrorHandler(status?: boolean): void;
4
4
  export declare function extractMethodNameFromStackTrace(stackTrace: string): string;
@@ -1,4 +1,4 @@
1
- /*! @zoho_apptics/apptics-js-sdk v2.0.0-beta | (c) Zoho Corporation Private Limited | see LICENSE */
1
+ /*! @zoho_apptics/apptics-js-sdk v2.0.0-beta.1 | (c) Zoho Corporation Private Limited | see LICENSE */
2
2
  'use strict';
3
3
 
4
4
  var constants = require('../core/constants.js');
@@ -38,19 +38,59 @@ function stackOf(errorEvent) {
38
38
  }
39
39
  /** Said in place of a stack when there is none, so `message` is never sent empty. */
40
40
  const NO_STACK = 'no stack available'; // no i18n
41
+ /** The element an event came from, as `<script src="…">`, so a failed load says what failed. */
42
+ function describeTarget(target) {
43
+ var el = target;
44
+ if (!el || typeof el.tagName !== 'string') { // no i18n
45
+ return ''; // no i18n
46
+ }
47
+ var tag = el.tagName.toLowerCase();
48
+ var url = el.currentSrc || el.src || el.href || ''; // no i18n
49
+ return ' on <' + tag + (url ? (el.href && !el.src ? ' href="' : ' src="') + url + '"' : '') + '>'; // no i18n
50
+ }
51
+ /**
52
+ * What was thrown or rejected, as text an issue can be named by. An Error reads as itself. A
53
+ * promise rejected with an Event - what a failed script or image load hands over - used to be
54
+ * recorded as "[object Event]"; it is named by its type and where it came from instead. Anything
55
+ * else that is an object is shown as its JSON, cut short.
56
+ */
57
+ function describeThrown(thrown) {
58
+ if (thrown === null || thrown === undefined) {
59
+ return ''; // no i18n
60
+ }
61
+ if (typeof thrown !== 'object') { // no i18n
62
+ return String(thrown);
63
+ }
64
+ if (thrown instanceof Error || typeof thrown.message === 'string' && typeof thrown.name === 'string') { // no i18n
65
+ return String(thrown);
66
+ }
67
+ if (typeof Event !== 'undefined' && thrown instanceof Event) { // no i18n
68
+ return 'Event "' + thrown.type + '"' + describeTarget(thrown.target); // no i18n
69
+ }
70
+ var text = String(thrown);
71
+ if (text !== '[object Object]') { // no i18n
72
+ return text;
73
+ }
74
+ try {
75
+ return JSON.stringify(thrown).slice(0, 300);
76
+ }
77
+ catch (err) {
78
+ return text;
79
+ }
80
+ }
41
81
  function trackError(errorEvent, jsonObj) {
42
82
  if (!consent.canTrackErrors('trackError')) { // no i18n
43
83
  return;
44
84
  }
45
85
  if (utils.isEmpty(errorEvent)) {
46
- logger.Console.warn('AppticsTrackError: Invalid error'); // no i18n
86
+ logger.Console.warn('AppticsError.trackError: Invalid error'); // no i18n
47
87
  return;
48
88
  }
49
89
  var _jsonObj = validate.validateJSON(jsonObj);
50
90
  var stack = stackOf(errorEvent);
51
91
  var methodName = extractMethodNameFromStackTrace(stack) || "";
52
92
  var issuetype = errorEvent.type || "";
53
- var thrown = errorEvent.reason || errorEvent.error || errorEvent.message || "";
93
+ var thrown = describeThrown(errorEvent.reason || errorEvent.error || errorEvent.message || "");
54
94
  // "Error: x at handler", or just "Error: x" when the stack names no method. issuename is
55
95
  // required too, so something that carried no message at all is still named rather than blank
56
96
  var described = !utils.isEmpty(String(thrown)) ? String(thrown) : (!utils.isEmpty(issuetype) ? String(issuetype) : 'Unknown error'); // no i18n
@@ -76,10 +116,10 @@ function trackError(errorEvent, jsonObj) {
76
116
  session.commitPendingSession();
77
117
  debug.noteDebugActivity();
78
118
  storage.error_transactions.add(errorData);
79
- logger.Console.log('AppticsTrackError(\'' + issuetype + '\',\'' + message + '\',' + JSON.stringify(_jsonObj || {}) + ')'); // no i18n
119
+ logger.Console.log('AppticsError.trackError(\'' + issuetype + '\',\'' + message + '\',' + JSON.stringify(_jsonObj || {}) + ')'); // no i18n
80
120
  logger.Console.dir(errorData);
81
121
  }
82
- function enableGlobalErrorHandler(status) {
122
+ function trackGlobalErrorHandler(status) {
83
123
  state._ZAP._isGlobalErrorTracking = typeof status === "boolean" ? status : true; // no i18n
84
124
  }
85
125
  function extractMethodNameFromStackTrace(stackTrace) {
@@ -122,6 +162,6 @@ function extractMethodNameFromStackTrace(stackTrace) {
122
162
  return methodName;
123
163
  }
124
164
 
125
- exports.enableGlobalErrorHandler = enableGlobalErrorHandler;
126
165
  exports.extractMethodNameFromStackTrace = extractMethodNameFromStackTrace;
127
166
  exports.trackError = trackError;
167
+ exports.trackGlobalErrorHandler = trackGlobalErrorHandler;
@@ -12,6 +12,12 @@ export declare const logChoice: {
12
12
  includeLog?: boolean;
13
13
  includeDyninfo?: boolean;
14
14
  };
15
+ /**
16
+ * Who a sendFeedback() report, made without the form, is attributed to: nobody when `anonymous`,
17
+ * else the user AppticsUser.setUser set (or the active registered one, status 1). Returns a function
18
+ * that puts back the choice that was there, so a form left open keeps its own.
19
+ */
20
+ export declare function chooseUserForSend(anonymous: boolean): () => void;
15
21
  /**
16
22
  * The "User" dropdown above the description. It lists Anonymous plus every registered user, and
17
23
  * starts on the active one (status 1) unless the caller asked for anonymous.
@@ -1,10 +1,11 @@
1
- /*! @zoho_apptics/apptics-js-sdk v2.0.0-beta | (c) Zoho Corporation Private Limited | see LICENSE */
1
+ /*! @zoho_apptics/apptics-js-sdk v2.0.0-beta.1 | (c) Zoho Corporation Private Limited | see LICENSE */
2
2
  'use strict';
3
3
 
4
4
  var logger = require('../core/logger.js');
5
5
  var hooks = require('../core/hooks.js');
6
6
  var storage = require('../core/storage.js');
7
7
  var utils = require('../core/utils.js');
8
+ var state = require('../core/state.js');
8
9
  var logs = require('./logs.js');
9
10
 
10
11
  /**
@@ -25,6 +26,34 @@ function resetUserChoice() {
25
26
  userChoice.userid = ''; // no i18n
26
27
  userChoice.groupid = ''; // no i18n
27
28
  }
29
+ /**
30
+ * Who a sendFeedback() report, made without the form, is attributed to: nobody when `anonymous`,
31
+ * else the user AppticsUser.setUser set (or the active registered one, status 1). Returns a function
32
+ * that puts back the choice that was there, so a form left open keeps its own.
33
+ */
34
+ function chooseUserForSend(anonymous) {
35
+ var before = { anonymous: userChoice.anonymous, userid: userChoice.userid, groupid: userChoice.groupid };
36
+ resetUserChoice();
37
+ if (!anonymous) {
38
+ // the user setUser set on this page, else the active one of those registered before
39
+ var active = !utils.isEmpty(state._ZAP._user_info.userid) ? state._ZAP._user_info : null;
40
+ storedUsers().forEach(function (user) {
41
+ if (!active && user && user.status == 1 && !utils.isEmpty(user.userid)) {
42
+ active = user;
43
+ }
44
+ });
45
+ if (active) {
46
+ userChoice.anonymous = false;
47
+ userChoice.userid = String(active.userid);
48
+ userChoice.groupid = String(active.groupid || ''); // no i18n
49
+ }
50
+ }
51
+ return function restore() {
52
+ userChoice.anonymous = before.anonymous;
53
+ userChoice.userid = before.userid;
54
+ userChoice.groupid = before.groupid;
55
+ };
56
+ }
28
57
  /** Every distinct registered user, by the address the picker shows. */
29
58
  function storedUsers() {
30
59
  var users = storage.user_defaults.item('users'); // no i18n
@@ -279,6 +308,7 @@ hooks.hooks.feedbackUser = function () {
279
308
  return userChoice.anonymous || utils.isEmpty(userChoice.userid) ? null : userChoice;
280
309
  };
281
310
 
311
+ exports.chooseUserForSend = chooseUserForSend;
282
312
  exports.logChoice = logChoice;
283
313
  exports.userChoice = userChoice;
284
314
  exports.wireDrag = wireDrag;