@atlaskit/smart-hooks 0.0.3 → 0.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @atlaskit/smart-hooks
2
2
 
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`6a3107dd76a`](https://bitbucket.org/atlassian/atlassian-frontend/commits/6a3107dd76a) - Adding analytics + UFO
8
+
3
9
  ## 0.0.3
4
10
 
5
11
  ### Patch Changes
@@ -11,21 +11,28 @@ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/sli
11
11
 
12
12
  var _react = require("react");
13
13
 
14
+ var _memoizeOne = _interopRequireDefault(require("memoize-one"));
15
+
14
16
  function useFunctionUsageTracking(trackedFunction) {
15
17
  var _useState = (0, _react.useState)(false),
16
18
  _useState2 = (0, _slicedToArray2.default)(_useState, 2),
17
19
  isUsed = _useState2[0],
18
- setIsUsed = _useState2[1];
20
+ setIsUsed = _useState2[1]; // trackingFunction needs to be stable for hook consumers...
21
+
19
22
 
20
23
  var trackingFunction = (0, _react.useMemo)(function () {
21
- return function () {
22
- if (!isUsed) {
23
- setIsUsed(true);
24
- }
25
-
26
- return trackedFunction;
27
- };
28
- }, [trackedFunction, isUsed]);
24
+ return (// ...and the function that the factory returns needs to be stable for hook consumers as well
25
+ (0, _memoizeOne.default)(function () {
26
+ if (!isUsed) {
27
+ setIsUsed(true);
28
+ }
29
+
30
+ return trackedFunction;
31
+ })
32
+ );
33
+ }, // isUsed deliberately left out to avoid new tracking fn being generated
34
+ // eslint-disable-next-line react-hooks/exhaustive-deps
35
+ [trackedFunction]);
29
36
  return {
30
37
  isUsed: isUsed,
31
38
  trackingFunction: trackingFunction
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.fireUserSelectedEvent = exports.findUserPosition = exports.createDefaultAttributes = void 0;
9
+
10
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
11
+
12
+ var _analyticsGasTypes = require("@atlaskit/analytics-gas-types");
13
+
14
+ var _analyticsListeners = require("@atlaskit/analytics-listeners");
15
+
16
+ var _analyticsNext = require("@atlaskit/analytics-next");
17
+
18
+ var _version = require("../../version.json");
19
+
20
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
21
+
22
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
23
+
24
+ var createAndFireEventInElementsChannel = (0, _analyticsNext.createAndFireEvent)(_analyticsListeners.FabricChannel.elements);
25
+
26
+ var createEvent = function createEvent(eventType, action, actionSubject) {
27
+ var attributes = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
28
+ return {
29
+ eventType: eventType,
30
+ action: action,
31
+ actionSubject: actionSubject,
32
+ source: '@atlaskit/smart-hooks/use-user-recommendations',
33
+ attributes: _objectSpread({
34
+ packageName: _version.name,
35
+ packageVersion: _version.version
36
+ }, attributes)
37
+ };
38
+ };
39
+
40
+ var findUserPosition = function findUserPosition(loadedUsers, userId) {
41
+ return loadedUsers.findIndex(function (val) {
42
+ return val.id === userId;
43
+ });
44
+ };
45
+
46
+ exports.findUserPosition = findUserPosition;
47
+
48
+ var createDefaultAttributes = function createDefaultAttributes(props, renderId, sessionId, query) {
49
+ var fieldId = props.fieldId,
50
+ objectId = props.objectId,
51
+ containerId = props.containerId,
52
+ childObjectId = props.childObjectId,
53
+ preload = props.preload,
54
+ includeTeams = props.includeTeams,
55
+ productKey = props.productKey,
56
+ principalId = props.principalId,
57
+ tenantId = props.tenantId,
58
+ maxNumberOfResults = props.maxNumberOfResults;
59
+ return {
60
+ context: fieldId,
61
+ childObjectId: childObjectId,
62
+ containerId: containerId,
63
+ includeTeams: includeTeams,
64
+ maxNumberOfResults: maxNumberOfResults,
65
+ objectId: objectId,
66
+ preload: preload,
67
+ principalId: principalId,
68
+ productKey: productKey,
69
+ queryLength: (query || '').length,
70
+ renderId: renderId,
71
+ sessionId: sessionId,
72
+ tenantId: tenantId
73
+ };
74
+ };
75
+
76
+ exports.createDefaultAttributes = createDefaultAttributes;
77
+
78
+ var fireUserSelectedEvent = function fireUserSelectedEvent(createAnalyticsEvent, payloadAttributes) {
79
+ return createAndFireEventInElementsChannel(createEvent(_analyticsGasTypes.TRACK_EVENT_TYPE, 'selected', 'user', payloadAttributes))(createAnalyticsEvent);
80
+ };
81
+
82
+ exports.fireUserSelectedEvent = fireUserSelectedEvent;
@@ -19,10 +19,18 @@ var _react = require("react");
19
19
 
20
20
  var _debounce = _interopRequireDefault(require("lodash/debounce"));
21
21
 
22
+ var _uuid = require("uuid");
23
+
24
+ var _analyticsNext = require("@atlaskit/analytics-next");
25
+
22
26
  var _smartCommon = require("@atlaskit/smart-common");
23
27
 
24
28
  var _useFunctionUsageTracking = _interopRequireDefault(require("../use-function-usage-tracking"));
25
29
 
30
+ var _analytics = require("./analytics");
31
+
32
+ var _ufoExperiences = require("./ufoExperiences");
33
+
26
34
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
27
35
 
28
36
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
@@ -77,16 +85,42 @@ var useUserRecommendations = function useUserRecommendations(props) {
77
85
  error = _useState6[0],
78
86
  setError = _useState6[1];
79
87
 
88
+ var renderId = (0, _react.useRef)('');
89
+ var sessionId = (0, _react.useRef)('');
90
+ var currentQuery = (0, _react.useRef)('');
91
+
92
+ var _useAnalyticsEvents = (0, _analyticsNext.useAnalyticsEvents)(),
93
+ createAnalyticsEvent = _useAnalyticsEvents.createAnalyticsEvent;
94
+
95
+ var getAnalyticsAttributes = (0, _react.useCallback)(function () {
96
+ return _objectSpread({}, (0, _analytics.createDefaultAttributes)({
97
+ fieldId: fieldId,
98
+ objectId: objectId,
99
+ containerId: containerId,
100
+ childObjectId: childObjectId,
101
+ preload: preload,
102
+ includeTeams: includeTeams,
103
+ productKey: productKey,
104
+ principalId: principalId,
105
+ tenantId: tenantId,
106
+ maxNumberOfResults: maxNumberOfResults
107
+ }, renderId.current, sessionId.current, currentQuery.current));
108
+ }, [childObjectId, containerId, fieldId, includeTeams, maxNumberOfResults, objectId, preload, principalId, productKey, tenantId]);
80
109
  var fetchRecommendations = (0, _react.useCallback)( /*#__PURE__*/function () {
81
- var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(query, sessionId) {
82
- var recommendedUsers;
110
+ var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(query) {
111
+ var recommendedUsersResult;
83
112
  return _regenerator.default.wrap(function _callee$(_context) {
84
113
  while (1) {
85
114
  switch (_context.prev = _context.next) {
86
115
  case 0:
87
- setIsLoading(true);
88
- _context.prev = 1;
89
- _context.next = 4;
116
+ setIsLoading(true); // restart session
117
+
118
+ sessionId.current = (0, _uuid.v4)();
119
+
120
+ _ufoExperiences.UsersFetchedUfoExperience.getInstance(fieldId).start();
121
+
122
+ _context.prev = 3;
123
+ _context.next = 6;
90
124
  return (0, _smartCommon.fetchUserRecommendations)({
91
125
  baseUrl: baseUrl,
92
126
  context: {
@@ -98,7 +132,7 @@ var useUserRecommendations = function useUserRecommendations(props) {
98
132
  productKey: productKey,
99
133
  contextType: fieldId,
100
134
  siteId: tenantId,
101
- sessionId: sessionId
135
+ sessionId: sessionId.current
102
136
  },
103
137
  includeUsers: includeUsers,
104
138
  includeGroups: includeGroups,
@@ -108,32 +142,45 @@ var useUserRecommendations = function useUserRecommendations(props) {
108
142
  query: query
109
143
  });
110
144
 
111
- case 4:
112
- recommendedUsers = _context.sent;
113
- setRecommendations(recommendedUsers);
114
- _context.next = 11;
145
+ case 6:
146
+ recommendedUsersResult = _context.sent;
147
+ currentQuery.current = query !== null && query !== void 0 ? query : '';
148
+
149
+ _ufoExperiences.UsersFetchedUfoExperience.getInstance(fieldId).success({
150
+ metadata: _objectSpread(_objectSpread({}, getAnalyticsAttributes()), {}, {
151
+ loadedUsersSize: recommendedUsersResult.length
152
+ })
153
+ });
154
+
155
+ setRecommendations(recommendedUsersResult);
156
+ _context.next = 16;
115
157
  break;
116
158
 
117
- case 8:
118
- _context.prev = 8;
119
- _context.t0 = _context["catch"](1);
159
+ case 12:
160
+ _context.prev = 12;
161
+ _context.t0 = _context["catch"](3);
162
+
163
+ _ufoExperiences.UsersFetchedUfoExperience.getInstance(fieldId).failure({
164
+ metadata: getAnalyticsAttributes()
165
+ });
166
+
120
167
  setError(_context.t0);
121
168
 
122
- case 11:
169
+ case 16:
123
170
  setIsLoading(false);
124
171
 
125
- case 12:
172
+ case 17:
126
173
  case "end":
127
174
  return _context.stop();
128
175
  }
129
176
  }
130
- }, _callee, null, [[1, 8]]);
177
+ }, _callee, null, [[3, 12]]);
131
178
  }));
132
179
 
133
- return function (_x, _x2) {
180
+ return function (_x) {
134
181
  return _ref.apply(this, arguments);
135
182
  };
136
- }(), [baseUrl, childObjectId, containerId, fieldId, includeGroups, includeTeams, includeUsers, maxNumberOfResults, objectId, principalId, productAttributes, productKey, cpusSearchQuery, tenantId]);
183
+ }(), [baseUrl, childObjectId, containerId, objectId, principalId, productAttributes, productKey, fieldId, tenantId, includeUsers, includeGroups, includeTeams, maxNumberOfResults, cpusSearchQuery, getAnalyticsAttributes]);
137
184
  var debouncedFetchRecommendations = (0, _react.useMemo)(function () {
138
185
  return (0, _debounce.default)(fetchRecommendations, debounceTimeMs);
139
186
  }, [fetchRecommendations, debounceTimeMs]);
@@ -141,26 +188,33 @@ var useUserRecommendations = function useUserRecommendations(props) {
141
188
  if (preload) {
142
189
  debouncedFetchRecommendations('');
143
190
  }
144
- }, [preload, debouncedFetchRecommendations]); // TODO - analytics
145
-
146
- var onInputChange = (0, _react.useCallback)(function (query, sessionId) {
147
- debouncedFetchRecommendations(query, sessionId);
148
- }, [debouncedFetchRecommendations]); // TODO - analytics
149
-
150
- var onChange = (0, _react.useCallback)(function (value, action) {}, []);
191
+ }, [preload, debouncedFetchRecommendations]);
192
+ var onInputChange = (0, _react.useCallback)(function (query) {
193
+ debouncedFetchRecommendations(query !== null && query !== void 0 ? query : '');
194
+ }, [debouncedFetchRecommendations]);
195
+ var onUserSelect = (0, _react.useCallback)(function (userId) {
196
+ (0, _analytics.fireUserSelectedEvent)(createAnalyticsEvent, _objectSpread(_objectSpread({}, getAnalyticsAttributes()), {}, {
197
+ selectedUser: userId,
198
+ loadedUsersSize: recommendations.length,
199
+ position: (0, _analytics.findUserPosition)(recommendations, userId)
200
+ }));
201
+ }, [createAnalyticsEvent, getAnalyticsAttributes, recommendations]);
151
202
 
152
203
  var _useFunctionUsageTrac = (0, _useFunctionUsageTracking.default)(onInputChange),
153
204
  isOnInputChangeUsed = _useFunctionUsageTrac.isUsed,
154
205
  usageTrackedOnInputChange = _useFunctionUsageTrac.trackingFunction;
155
206
 
156
- var _useFunctionUsageTrac2 = (0, _useFunctionUsageTracking.default)(onChange),
157
- isOnChangeUsed = _useFunctionUsageTrac2.isUsed,
158
- usageTrackedOnChange = _useFunctionUsageTrac2.trackingFunction;
207
+ var _useFunctionUsageTrac2 = (0, _useFunctionUsageTracking.default)(onUserSelect),
208
+ isOnUserSelectUsed = _useFunctionUsageTrac2.isUsed,
209
+ usageTrackedOnUserSelect = _useFunctionUsageTrac2.trackingFunction;
159
210
 
211
+ (0, _react.useEffect)(function () {
212
+ renderId.current = (0, _uuid.v4)();
213
+ }, []);
160
214
  return {
161
- recommendations: isOnInputChangeUsed && isOnChangeUsed ? recommendations : instrumentFailureOption,
162
- onInputChange: usageTrackedOnInputChange,
163
- onChange: usageTrackedOnChange,
215
+ recommendations: isOnInputChangeUsed && isOnUserSelectUsed ? recommendations : instrumentFailureOption,
216
+ triggerSearchFactory: usageTrackedOnInputChange,
217
+ selectUserFactory: usageTrackedOnUserSelect,
164
218
  isLoading: isLoading,
165
219
  error: error
166
220
  };
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.UsersFetchedUfoExperience = void 0;
7
+
8
+ var _ufo = require("@atlaskit/ufo");
9
+
10
+ var COMPONENT_NAME = 'smart-hooks.use-user-recommendations';
11
+ var UsersFetchedUfoExperience = new _ufo.ConcurrentExperience('users-fetched', {
12
+ platform: {
13
+ component: COMPONENT_NAME
14
+ },
15
+ type: _ufo.ExperienceTypes.Load,
16
+ performanceType: _ufo.ExperiencePerformanceTypes.InlineResult
17
+ });
18
+ exports.UsersFetchedUfoExperience = UsersFetchedUfoExperience;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/smart-hooks",
3
- "version": "0.0.3",
3
+ "version": "0.1.0",
4
4
  "sideEffects": false
5
5
  }
@@ -1,14 +1,19 @@
1
1
  import { useMemo, useState } from 'react';
2
+ import memoizeOne from 'memoize-one';
2
3
 
3
4
  function useFunctionUsageTracking(trackedFunction) {
4
- const [isUsed, setIsUsed] = useState(false);
5
- const trackingFunction = useMemo(() => () => {
5
+ const [isUsed, setIsUsed] = useState(false); // trackingFunction needs to be stable for hook consumers...
6
+
7
+ const trackingFunction = useMemo(() => // ...and the function that the factory returns needs to be stable for hook consumers as well
8
+ memoizeOne(() => {
6
9
  if (!isUsed) {
7
10
  setIsUsed(true);
8
11
  }
9
12
 
10
13
  return trackedFunction;
11
- }, [trackedFunction, isUsed]);
14
+ }), // isUsed deliberately left out to avoid new tracking fn being generated
15
+ // eslint-disable-next-line react-hooks/exhaustive-deps
16
+ [trackedFunction]);
12
17
  return {
13
18
  isUsed,
14
19
  trackingFunction
@@ -0,0 +1,51 @@
1
+ import { TRACK_EVENT_TYPE } from '@atlaskit/analytics-gas-types';
2
+ import { FabricChannel } from '@atlaskit/analytics-listeners';
3
+ import { createAndFireEvent } from '@atlaskit/analytics-next';
4
+ import { name as packageName, version as packageVersion } from '../../version.json';
5
+ const createAndFireEventInElementsChannel = createAndFireEvent(FabricChannel.elements);
6
+
7
+ const createEvent = (eventType, action, actionSubject, attributes = {}) => ({
8
+ eventType,
9
+ action,
10
+ actionSubject,
11
+ source: '@atlaskit/smart-hooks/use-user-recommendations',
12
+ attributes: {
13
+ packageName,
14
+ packageVersion,
15
+ ...attributes
16
+ }
17
+ });
18
+
19
+ export const findUserPosition = (loadedUsers, userId) => {
20
+ return loadedUsers.findIndex(val => val.id === userId);
21
+ };
22
+ export const createDefaultAttributes = (props, renderId, sessionId, query) => {
23
+ const {
24
+ fieldId,
25
+ objectId,
26
+ containerId,
27
+ childObjectId,
28
+ preload,
29
+ includeTeams,
30
+ productKey,
31
+ principalId,
32
+ tenantId,
33
+ maxNumberOfResults
34
+ } = props;
35
+ return {
36
+ context: fieldId,
37
+ childObjectId,
38
+ containerId,
39
+ includeTeams,
40
+ maxNumberOfResults,
41
+ objectId,
42
+ preload,
43
+ principalId,
44
+ productKey,
45
+ queryLength: (query || '').length,
46
+ renderId,
47
+ sessionId,
48
+ tenantId
49
+ };
50
+ };
51
+ export const fireUserSelectedEvent = (createAnalyticsEvent, payloadAttributes) => createAndFireEventInElementsChannel(createEvent(TRACK_EVENT_TYPE, 'selected', 'user', payloadAttributes))(createAnalyticsEvent);
@@ -1,7 +1,11 @@
1
- import { useCallback, useEffect, useMemo, useState } from 'react';
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import debounce from 'lodash/debounce';
3
+ import { v4 as uuid } from 'uuid';
4
+ import { useAnalyticsEvents } from '@atlaskit/analytics-next';
3
5
  import { EntityType, fetchUserRecommendations } from '@atlaskit/smart-common';
4
6
  import useFunctionUsageTracking from '../use-function-usage-tracking';
7
+ import { createDefaultAttributes, findUserPosition, fireUserSelectedEvent } from './analytics';
8
+ import { UsersFetchedUfoExperience } from './ufoExperiences';
5
9
  const MAX_NUMBER_RESULTS = 25;
6
10
  const DEFAULT_DEBOUNCE_TIME_MS = 150;
7
11
  const defaultProps = {
@@ -41,11 +45,33 @@ const useUserRecommendations = props => {
41
45
  const [recommendations, setRecommendations] = useState([]);
42
46
  const [isLoading, setIsLoading] = useState(false);
43
47
  const [error, setError] = useState();
44
- const fetchRecommendations = useCallback(async (query, sessionId) => {
45
- setIsLoading(true);
48
+ const renderId = useRef('');
49
+ const sessionId = useRef('');
50
+ const currentQuery = useRef('');
51
+ const {
52
+ createAnalyticsEvent
53
+ } = useAnalyticsEvents();
54
+ const getAnalyticsAttributes = useCallback(() => ({ ...createDefaultAttributes({
55
+ fieldId,
56
+ objectId,
57
+ containerId,
58
+ childObjectId,
59
+ preload,
60
+ includeTeams,
61
+ productKey,
62
+ principalId,
63
+ tenantId,
64
+ maxNumberOfResults
65
+ }, renderId.current, sessionId.current, currentQuery.current)
66
+ }), [childObjectId, containerId, fieldId, includeTeams, maxNumberOfResults, objectId, preload, principalId, productKey, tenantId]);
67
+ const fetchRecommendations = useCallback(async query => {
68
+ setIsLoading(true); // restart session
69
+
70
+ sessionId.current = uuid();
71
+ UsersFetchedUfoExperience.getInstance(fieldId).start();
46
72
 
47
73
  try {
48
- const recommendedUsers = await fetchUserRecommendations({
74
+ const recommendedUsersResult = await fetchUserRecommendations({
49
75
  baseUrl,
50
76
  context: {
51
77
  childObjectId,
@@ -56,7 +82,7 @@ const useUserRecommendations = props => {
56
82
  productKey,
57
83
  contextType: fieldId,
58
84
  siteId: tenantId,
59
- sessionId: sessionId
85
+ sessionId: sessionId.current
60
86
  },
61
87
  includeUsers,
62
88
  includeGroups,
@@ -65,37 +91,53 @@ const useUserRecommendations = props => {
65
91
  searchQuery: cpusSearchQuery,
66
92
  query
67
93
  });
68
- setRecommendations(recommendedUsers);
94
+ currentQuery.current = query !== null && query !== void 0 ? query : '';
95
+ UsersFetchedUfoExperience.getInstance(fieldId).success({
96
+ metadata: { ...getAnalyticsAttributes(),
97
+ loadedUsersSize: recommendedUsersResult.length
98
+ }
99
+ });
100
+ setRecommendations(recommendedUsersResult);
69
101
  } catch (error) {
102
+ UsersFetchedUfoExperience.getInstance(fieldId).failure({
103
+ metadata: getAnalyticsAttributes()
104
+ });
70
105
  setError(error);
71
106
  }
72
107
 
73
108
  setIsLoading(false);
74
- }, [baseUrl, childObjectId, containerId, fieldId, includeGroups, includeTeams, includeUsers, maxNumberOfResults, objectId, principalId, productAttributes, productKey, cpusSearchQuery, tenantId]);
109
+ }, [baseUrl, childObjectId, containerId, objectId, principalId, productAttributes, productKey, fieldId, tenantId, includeUsers, includeGroups, includeTeams, maxNumberOfResults, cpusSearchQuery, getAnalyticsAttributes]);
75
110
  const debouncedFetchRecommendations = useMemo(() => debounce(fetchRecommendations, debounceTimeMs), [fetchRecommendations, debounceTimeMs]);
76
111
  useEffect(() => {
77
112
  if (preload) {
78
113
  debouncedFetchRecommendations('');
79
114
  }
80
- }, [preload, debouncedFetchRecommendations]); // TODO - analytics
81
-
82
- const onInputChange = useCallback((query, sessionId) => {
83
- debouncedFetchRecommendations(query, sessionId);
84
- }, [debouncedFetchRecommendations]); // TODO - analytics
85
-
86
- const onChange = useCallback((value, action) => {}, []);
115
+ }, [preload, debouncedFetchRecommendations]);
116
+ const onInputChange = useCallback(query => {
117
+ debouncedFetchRecommendations(query !== null && query !== void 0 ? query : '');
118
+ }, [debouncedFetchRecommendations]);
119
+ const onUserSelect = useCallback(userId => {
120
+ fireUserSelectedEvent(createAnalyticsEvent, { ...getAnalyticsAttributes(),
121
+ selectedUser: userId,
122
+ loadedUsersSize: recommendations.length,
123
+ position: findUserPosition(recommendations, userId)
124
+ });
125
+ }, [createAnalyticsEvent, getAnalyticsAttributes, recommendations]);
87
126
  const {
88
127
  isUsed: isOnInputChangeUsed,
89
128
  trackingFunction: usageTrackedOnInputChange
90
129
  } = useFunctionUsageTracking(onInputChange);
91
130
  const {
92
- isUsed: isOnChangeUsed,
93
- trackingFunction: usageTrackedOnChange
94
- } = useFunctionUsageTracking(onChange);
131
+ isUsed: isOnUserSelectUsed,
132
+ trackingFunction: usageTrackedOnUserSelect
133
+ } = useFunctionUsageTracking(onUserSelect);
134
+ useEffect(() => {
135
+ renderId.current = uuid();
136
+ }, []);
95
137
  return {
96
- recommendations: isOnInputChangeUsed && isOnChangeUsed ? recommendations : instrumentFailureOption,
97
- onInputChange: usageTrackedOnInputChange,
98
- onChange: usageTrackedOnChange,
138
+ recommendations: isOnInputChangeUsed && isOnUserSelectUsed ? recommendations : instrumentFailureOption,
139
+ triggerSearchFactory: usageTrackedOnInputChange,
140
+ selectUserFactory: usageTrackedOnUserSelect,
99
141
  isLoading,
100
142
  error
101
143
  };
@@ -0,0 +1,9 @@
1
+ import { ConcurrentExperience, ExperiencePerformanceTypes, ExperienceTypes } from '@atlaskit/ufo';
2
+ const COMPONENT_NAME = 'smart-hooks.use-user-recommendations';
3
+ export const UsersFetchedUfoExperience = new ConcurrentExperience('users-fetched', {
4
+ platform: {
5
+ component: COMPONENT_NAME
6
+ },
7
+ type: ExperienceTypes.Load,
8
+ performanceType: ExperiencePerformanceTypes.InlineResult
9
+ });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/smart-hooks",
3
- "version": "0.0.3",
3
+ "version": "0.1.0",
4
4
  "sideEffects": false
5
5
  }
@@ -1,21 +1,27 @@
1
1
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
2
  import { useMemo, useState } from 'react';
3
+ import memoizeOne from 'memoize-one';
3
4
 
4
5
  function useFunctionUsageTracking(trackedFunction) {
5
6
  var _useState = useState(false),
6
7
  _useState2 = _slicedToArray(_useState, 2),
7
8
  isUsed = _useState2[0],
8
- setIsUsed = _useState2[1];
9
+ setIsUsed = _useState2[1]; // trackingFunction needs to be stable for hook consumers...
10
+
9
11
 
10
12
  var trackingFunction = useMemo(function () {
11
- return function () {
12
- if (!isUsed) {
13
- setIsUsed(true);
14
- }
13
+ return (// ...and the function that the factory returns needs to be stable for hook consumers as well
14
+ memoizeOne(function () {
15
+ if (!isUsed) {
16
+ setIsUsed(true);
17
+ }
15
18
 
16
- return trackedFunction;
17
- };
18
- }, [trackedFunction, isUsed]);
19
+ return trackedFunction;
20
+ })
21
+ );
22
+ }, // isUsed deliberately left out to avoid new tracking fn being generated
23
+ // eslint-disable-next-line react-hooks/exhaustive-deps
24
+ [trackedFunction]);
19
25
  return {
20
26
  isUsed: isUsed,
21
27
  trackingFunction: trackingFunction
@@ -0,0 +1,61 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+
3
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
4
+
5
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
6
+
7
+ import { TRACK_EVENT_TYPE } from '@atlaskit/analytics-gas-types';
8
+ import { FabricChannel } from '@atlaskit/analytics-listeners';
9
+ import { createAndFireEvent } from '@atlaskit/analytics-next';
10
+ import { name as packageName, version as packageVersion } from '../../version.json';
11
+ var createAndFireEventInElementsChannel = createAndFireEvent(FabricChannel.elements);
12
+
13
+ var createEvent = function createEvent(eventType, action, actionSubject) {
14
+ var attributes = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
15
+ return {
16
+ eventType: eventType,
17
+ action: action,
18
+ actionSubject: actionSubject,
19
+ source: '@atlaskit/smart-hooks/use-user-recommendations',
20
+ attributes: _objectSpread({
21
+ packageName: packageName,
22
+ packageVersion: packageVersion
23
+ }, attributes)
24
+ };
25
+ };
26
+
27
+ export var findUserPosition = function findUserPosition(loadedUsers, userId) {
28
+ return loadedUsers.findIndex(function (val) {
29
+ return val.id === userId;
30
+ });
31
+ };
32
+ export var createDefaultAttributes = function createDefaultAttributes(props, renderId, sessionId, query) {
33
+ var fieldId = props.fieldId,
34
+ objectId = props.objectId,
35
+ containerId = props.containerId,
36
+ childObjectId = props.childObjectId,
37
+ preload = props.preload,
38
+ includeTeams = props.includeTeams,
39
+ productKey = props.productKey,
40
+ principalId = props.principalId,
41
+ tenantId = props.tenantId,
42
+ maxNumberOfResults = props.maxNumberOfResults;
43
+ return {
44
+ context: fieldId,
45
+ childObjectId: childObjectId,
46
+ containerId: containerId,
47
+ includeTeams: includeTeams,
48
+ maxNumberOfResults: maxNumberOfResults,
49
+ objectId: objectId,
50
+ preload: preload,
51
+ principalId: principalId,
52
+ productKey: productKey,
53
+ queryLength: (query || '').length,
54
+ renderId: renderId,
55
+ sessionId: sessionId,
56
+ tenantId: tenantId
57
+ };
58
+ };
59
+ export var fireUserSelectedEvent = function fireUserSelectedEvent(createAnalyticsEvent, payloadAttributes) {
60
+ return createAndFireEventInElementsChannel(createEvent(TRACK_EVENT_TYPE, 'selected', 'user', payloadAttributes))(createAnalyticsEvent);
61
+ };
@@ -7,10 +7,14 @@ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (O
7
7
 
8
8
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
9
9
 
10
- import { useCallback, useEffect, useMemo, useState } from 'react';
10
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
11
11
  import debounce from 'lodash/debounce';
12
+ import { v4 as uuid } from 'uuid';
13
+ import { useAnalyticsEvents } from '@atlaskit/analytics-next';
12
14
  import { EntityType, fetchUserRecommendations } from '@atlaskit/smart-common';
13
15
  import useFunctionUsageTracking from '../use-function-usage-tracking';
16
+ import { createDefaultAttributes, findUserPosition, fireUserSelectedEvent } from './analytics';
17
+ import { UsersFetchedUfoExperience } from './ufoExperiences';
14
18
  var MAX_NUMBER_RESULTS = 25;
15
19
  var DEFAULT_DEBOUNCE_TIME_MS = 150;
16
20
  var defaultProps = {
@@ -60,16 +64,40 @@ var useUserRecommendations = function useUserRecommendations(props) {
60
64
  error = _useState6[0],
61
65
  setError = _useState6[1];
62
66
 
67
+ var renderId = useRef('');
68
+ var sessionId = useRef('');
69
+ var currentQuery = useRef('');
70
+
71
+ var _useAnalyticsEvents = useAnalyticsEvents(),
72
+ createAnalyticsEvent = _useAnalyticsEvents.createAnalyticsEvent;
73
+
74
+ var getAnalyticsAttributes = useCallback(function () {
75
+ return _objectSpread({}, createDefaultAttributes({
76
+ fieldId: fieldId,
77
+ objectId: objectId,
78
+ containerId: containerId,
79
+ childObjectId: childObjectId,
80
+ preload: preload,
81
+ includeTeams: includeTeams,
82
+ productKey: productKey,
83
+ principalId: principalId,
84
+ tenantId: tenantId,
85
+ maxNumberOfResults: maxNumberOfResults
86
+ }, renderId.current, sessionId.current, currentQuery.current));
87
+ }, [childObjectId, containerId, fieldId, includeTeams, maxNumberOfResults, objectId, preload, principalId, productKey, tenantId]);
63
88
  var fetchRecommendations = useCallback( /*#__PURE__*/function () {
64
- var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(query, sessionId) {
65
- var recommendedUsers;
89
+ var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(query) {
90
+ var recommendedUsersResult;
66
91
  return _regeneratorRuntime.wrap(function _callee$(_context) {
67
92
  while (1) {
68
93
  switch (_context.prev = _context.next) {
69
94
  case 0:
70
- setIsLoading(true);
71
- _context.prev = 1;
72
- _context.next = 4;
95
+ setIsLoading(true); // restart session
96
+
97
+ sessionId.current = uuid();
98
+ UsersFetchedUfoExperience.getInstance(fieldId).start();
99
+ _context.prev = 3;
100
+ _context.next = 6;
73
101
  return fetchUserRecommendations({
74
102
  baseUrl: baseUrl,
75
103
  context: {
@@ -81,7 +109,7 @@ var useUserRecommendations = function useUserRecommendations(props) {
81
109
  productKey: productKey,
82
110
  contextType: fieldId,
83
111
  siteId: tenantId,
84
- sessionId: sessionId
112
+ sessionId: sessionId.current
85
113
  },
86
114
  includeUsers: includeUsers,
87
115
  includeGroups: includeGroups,
@@ -91,32 +119,41 @@ var useUserRecommendations = function useUserRecommendations(props) {
91
119
  query: query
92
120
  });
93
121
 
94
- case 4:
95
- recommendedUsers = _context.sent;
96
- setRecommendations(recommendedUsers);
97
- _context.next = 11;
122
+ case 6:
123
+ recommendedUsersResult = _context.sent;
124
+ currentQuery.current = query !== null && query !== void 0 ? query : '';
125
+ UsersFetchedUfoExperience.getInstance(fieldId).success({
126
+ metadata: _objectSpread(_objectSpread({}, getAnalyticsAttributes()), {}, {
127
+ loadedUsersSize: recommendedUsersResult.length
128
+ })
129
+ });
130
+ setRecommendations(recommendedUsersResult);
131
+ _context.next = 16;
98
132
  break;
99
133
 
100
- case 8:
101
- _context.prev = 8;
102
- _context.t0 = _context["catch"](1);
134
+ case 12:
135
+ _context.prev = 12;
136
+ _context.t0 = _context["catch"](3);
137
+ UsersFetchedUfoExperience.getInstance(fieldId).failure({
138
+ metadata: getAnalyticsAttributes()
139
+ });
103
140
  setError(_context.t0);
104
141
 
105
- case 11:
142
+ case 16:
106
143
  setIsLoading(false);
107
144
 
108
- case 12:
145
+ case 17:
109
146
  case "end":
110
147
  return _context.stop();
111
148
  }
112
149
  }
113
- }, _callee, null, [[1, 8]]);
150
+ }, _callee, null, [[3, 12]]);
114
151
  }));
115
152
 
116
- return function (_x, _x2) {
153
+ return function (_x) {
117
154
  return _ref.apply(this, arguments);
118
155
  };
119
- }(), [baseUrl, childObjectId, containerId, fieldId, includeGroups, includeTeams, includeUsers, maxNumberOfResults, objectId, principalId, productAttributes, productKey, cpusSearchQuery, tenantId]);
156
+ }(), [baseUrl, childObjectId, containerId, objectId, principalId, productAttributes, productKey, fieldId, tenantId, includeUsers, includeGroups, includeTeams, maxNumberOfResults, cpusSearchQuery, getAnalyticsAttributes]);
120
157
  var debouncedFetchRecommendations = useMemo(function () {
121
158
  return debounce(fetchRecommendations, debounceTimeMs);
122
159
  }, [fetchRecommendations, debounceTimeMs]);
@@ -124,26 +161,33 @@ var useUserRecommendations = function useUserRecommendations(props) {
124
161
  if (preload) {
125
162
  debouncedFetchRecommendations('');
126
163
  }
127
- }, [preload, debouncedFetchRecommendations]); // TODO - analytics
128
-
129
- var onInputChange = useCallback(function (query, sessionId) {
130
- debouncedFetchRecommendations(query, sessionId);
131
- }, [debouncedFetchRecommendations]); // TODO - analytics
132
-
133
- var onChange = useCallback(function (value, action) {}, []);
164
+ }, [preload, debouncedFetchRecommendations]);
165
+ var onInputChange = useCallback(function (query) {
166
+ debouncedFetchRecommendations(query !== null && query !== void 0 ? query : '');
167
+ }, [debouncedFetchRecommendations]);
168
+ var onUserSelect = useCallback(function (userId) {
169
+ fireUserSelectedEvent(createAnalyticsEvent, _objectSpread(_objectSpread({}, getAnalyticsAttributes()), {}, {
170
+ selectedUser: userId,
171
+ loadedUsersSize: recommendations.length,
172
+ position: findUserPosition(recommendations, userId)
173
+ }));
174
+ }, [createAnalyticsEvent, getAnalyticsAttributes, recommendations]);
134
175
 
135
176
  var _useFunctionUsageTrac = useFunctionUsageTracking(onInputChange),
136
177
  isOnInputChangeUsed = _useFunctionUsageTrac.isUsed,
137
178
  usageTrackedOnInputChange = _useFunctionUsageTrac.trackingFunction;
138
179
 
139
- var _useFunctionUsageTrac2 = useFunctionUsageTracking(onChange),
140
- isOnChangeUsed = _useFunctionUsageTrac2.isUsed,
141
- usageTrackedOnChange = _useFunctionUsageTrac2.trackingFunction;
180
+ var _useFunctionUsageTrac2 = useFunctionUsageTracking(onUserSelect),
181
+ isOnUserSelectUsed = _useFunctionUsageTrac2.isUsed,
182
+ usageTrackedOnUserSelect = _useFunctionUsageTrac2.trackingFunction;
142
183
 
184
+ useEffect(function () {
185
+ renderId.current = uuid();
186
+ }, []);
143
187
  return {
144
- recommendations: isOnInputChangeUsed && isOnChangeUsed ? recommendations : instrumentFailureOption,
145
- onInputChange: usageTrackedOnInputChange,
146
- onChange: usageTrackedOnChange,
188
+ recommendations: isOnInputChangeUsed && isOnUserSelectUsed ? recommendations : instrumentFailureOption,
189
+ triggerSearchFactory: usageTrackedOnInputChange,
190
+ selectUserFactory: usageTrackedOnUserSelect,
147
191
  isLoading: isLoading,
148
192
  error: error
149
193
  };
@@ -0,0 +1,9 @@
1
+ import { ConcurrentExperience, ExperiencePerformanceTypes, ExperienceTypes } from '@atlaskit/ufo';
2
+ var COMPONENT_NAME = 'smart-hooks.use-user-recommendations';
3
+ export var UsersFetchedUfoExperience = new ConcurrentExperience('users-fetched', {
4
+ platform: {
5
+ component: COMPONENT_NAME
6
+ },
7
+ type: ExperienceTypes.Load,
8
+ performanceType: ExperiencePerformanceTypes.InlineResult
9
+ });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/smart-hooks",
3
- "version": "0.0.3",
3
+ "version": "0.1.0",
4
4
  "sideEffects": false
5
5
  }
@@ -1,5 +1,5 @@
1
1
  declare function useFunctionUsageTracking<InputFunctionType extends (...any: any) => any>(trackedFunction: InputFunctionType): {
2
2
  isUsed: boolean;
3
- trackingFunction: () => InputFunctionType;
3
+ trackingFunction: import("memoize-one").MemoizedFn<() => InputFunctionType>;
4
4
  };
5
5
  export default useFunctionUsageTracking;
@@ -0,0 +1,20 @@
1
+ import { AnalyticsEventPayload, CreateUIAnalyticsEvent } from '@atlaskit/analytics-next';
2
+ import { UserSearchItem } from '@atlaskit/smart-common';
3
+ import { UseUserRecommendationsProps } from '../../types';
4
+ export declare const findUserPosition: (loadedUsers: UserSearchItem[], userId: string) => number;
5
+ export declare const createDefaultAttributes: (props: UseUserRecommendationsProps, renderId: string, sessionId: string, query: string) => {
6
+ context: string;
7
+ childObjectId: string | undefined;
8
+ containerId: string | undefined;
9
+ includeTeams: boolean | undefined;
10
+ maxNumberOfResults: number | undefined;
11
+ objectId: string | undefined;
12
+ preload: boolean | undefined;
13
+ principalId: string | undefined;
14
+ productKey: string;
15
+ queryLength: number;
16
+ renderId: string;
17
+ sessionId: string;
18
+ tenantId: string;
19
+ };
20
+ export declare const fireUserSelectedEvent: (createAnalyticsEvent: CreateUIAnalyticsEvent, payloadAttributes: AnalyticsEventPayload) => import("@atlaskit/analytics-next").UIAnalyticsEvent;
@@ -1,5 +1,4 @@
1
1
  import { EntityType, UserSearchItem } from '@atlaskit/smart-common';
2
- import { ActionTypes, Value } from '@atlaskit/user-picker';
3
2
  import type { UseUserRecommendationsProps } from '../../types';
4
3
  export declare const instrumentFailureOption: {
5
4
  id: string;
@@ -8,14 +7,14 @@ export declare const instrumentFailureOption: {
8
7
  avatarUrl: string;
9
8
  }[];
10
9
  declare const useUserRecommendations: (props: UseUserRecommendationsProps) => {
11
- recommendations: {
10
+ recommendations: UserSearchItem[] | {
12
11
  id: string;
13
12
  name: string;
14
13
  entityType: EntityType;
15
14
  avatarUrl: string;
16
- }[] | UserSearchItem[];
17
- onInputChange: () => (query?: string | undefined, sessionId?: string | undefined) => void;
18
- onChange: () => (value: Value, action: ActionTypes) => void;
15
+ }[];
16
+ triggerSearchFactory: import("memoize-one").MemoizedFn<() => (query?: string | undefined) => void>;
17
+ selectUserFactory: import("memoize-one").MemoizedFn<() => (userId: string) => void>;
19
18
  isLoading: boolean;
20
19
  error: any;
21
20
  };
@@ -0,0 +1,2 @@
1
+ import { ConcurrentExperience } from '@atlaskit/ufo';
2
+ export declare const UsersFetchedUfoExperience: ConcurrentExperience;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/smart-hooks",
3
- "version": "0.0.3",
3
+ "version": "0.1.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/"
6
6
  },
@@ -18,19 +18,22 @@
18
18
  "releaseModel": "continuous"
19
19
  },
20
20
  "dependencies": {
21
+ "@atlaskit/analytics-gas-types": "^5.0.5",
22
+ "@atlaskit/analytics-listeners": "^8.1.3",
23
+ "@atlaskit/analytics-next": "^8.0.0",
21
24
  "@atlaskit/smart-common": "^0.0.3",
22
- "@atlaskit/user-picker": "^8.3.0",
25
+ "@atlaskit/ufo": "^0.0.6",
26
+ "@atlaskit/user-picker": "^8.4.0",
23
27
  "@babel/runtime": "^7.0.0",
24
- "lodash": "^4.17.21"
28
+ "lodash": "^4.17.21",
29
+ "memoize-one": "^6.0.0",
30
+ "uuid": "^3.1.0"
25
31
  },
26
32
  "peerDependencies": {
27
33
  "react": "^16.8.0",
28
34
  "react-intl-next": "npm:react-intl@^5.18.1"
29
35
  },
30
36
  "devDependencies": {
31
- "@atlaskit/analytics-gas-types": "^5.0.0",
32
- "@atlaskit/analytics-listeners": "^8.0.0",
33
- "@atlaskit/analytics-next": "^8.2.0",
34
37
  "@atlaskit/analytics-viewer": "^0.4.5",
35
38
  "@atlaskit/button": "^16.0.0",
36
39
  "@atlaskit/docs": "*",
@@ -40,10 +43,12 @@
40
43
  "@atlaskit/visual-regression": "*",
41
44
  "@atlaskit/webdriver-runner": "*",
42
45
  "@atlassian/atlassian-frontend-prettier-config-1.0.1": "npm:@atlassian/atlassian-frontend-prettier-config@1.0.1",
46
+ "@atlassian/ufo": "^0.1.0",
43
47
  "@atlassiansox/analytics-web-client": "^4.2.0",
44
48
  "@storybook/addon-knobs": "^5.3.18",
45
49
  "@testing-library/react": "^8.0.1",
46
50
  "@testing-library/react-hooks": "^1.0.4",
51
+ "@welldone-software/why-did-you-render": "^6.2.0",
47
52
  "enzyme": "^3.10.0",
48
53
  "fetch-mock": "^8.0.0",
49
54
  "react-dom": "^16.8.0",