@constructor-io/constructorio-ui-pia 2.13.1 → 2.14.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/README.md +49 -1
- package/dist/constructorio-ui-pia-bundled.js +41 -41
- package/lib/cjs/components/CioPia/CioPia.js +6 -1
- package/lib/cjs/components/CioPia/CioPiaQna.js +2 -1
- package/lib/cjs/components/CioPiaControl/CioPiaControl.js +38 -0
- package/lib/cjs/components/CioPiaRecs/CioPiaRecs.js +3 -1
- package/lib/cjs/hooks/useCioPia.js +2 -1
- package/lib/cjs/hooks/usePiaClient.js +14 -7
- package/lib/cjs/hooks/useViewportTracking.js +9 -4
- package/lib/cjs/version.js +1 -1
- package/lib/mjs/components/CioPia/CioPia.js +5 -0
- package/lib/mjs/components/CioPia/CioPiaQna.js +2 -1
- package/lib/mjs/components/CioPiaControl/CioPiaControl.js +34 -0
- package/lib/mjs/components/CioPiaRecs/CioPiaRecs.js +3 -1
- package/lib/mjs/hooks/useCioPia.js +2 -1
- package/lib/mjs/hooks/usePiaClient.js +15 -3
- package/lib/mjs/hooks/useViewportTracking.js +9 -4
- package/lib/mjs/version.js +1 -1
- package/lib/types/components/CioPia/types.d.ts +3 -1
- package/lib/types/components/CioPiaControl/CioPiaControl.d.ts +9 -0
- package/lib/types/hooks/useCioPia.d.ts +2 -0
- package/lib/types/hooks/usePiaClient.d.ts +2 -1
- package/lib/types/hooks/useViewportTracking.d.ts +6 -1
- package/lib/types/types.d.ts +33 -0
- package/lib/types/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -4,9 +4,14 @@ const tslib_1 = require("tslib");
|
|
|
4
4
|
const react_1 = tslib_1.__importDefault(require("react"));
|
|
5
5
|
const CioPiaQna_1 = tslib_1.__importDefault(require("./CioPiaQna"));
|
|
6
6
|
const CioPiaRecs_1 = tslib_1.__importDefault(require("../CioPiaRecs/CioPiaRecs"));
|
|
7
|
+
const CioPiaControl_1 = tslib_1.__importDefault(require("../CioPiaControl/CioPiaControl"));
|
|
7
8
|
function CioPia(props) {
|
|
8
|
-
var _a, _b;
|
|
9
|
+
var _a, _b, _c;
|
|
9
10
|
const mode = (_b = (_a = props.displayConfigs) === null || _a === void 0 ? void 0 : _a.mode) !== null && _b !== void 0 ? _b : 'default';
|
|
11
|
+
// Before the mode dispatch: a control shopper gets no widget in any mode.
|
|
12
|
+
if ((_c = props.abTest) === null || _c === void 0 ? void 0 : _c.isControl) {
|
|
13
|
+
return react_1.default.createElement(CioPiaControl_1.default, Object.assign({}, props));
|
|
14
|
+
}
|
|
10
15
|
if (mode === 'recommendations') {
|
|
11
16
|
return react_1.default.createElement(CioPiaRecs_1.default, Object.assign({}, props));
|
|
12
17
|
}
|
|
@@ -22,7 +22,7 @@ const StatusRegion_1 = tslib_1.__importStar(require("../StatusRegion/StatusRegio
|
|
|
22
22
|
// eslint-disable-next-line complexity
|
|
23
23
|
function CioPiaQna(props) {
|
|
24
24
|
var _a;
|
|
25
|
-
const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, answerParameters, parameters, trackingConfigs, } = props;
|
|
25
|
+
const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, answerParameters, parameters, trackingConfigs, abTest, } = props;
|
|
26
26
|
const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline', showPreviousItems, disclaimerPosition = 'bottom', } = displayConfigs || {};
|
|
27
27
|
const isConversation = mode === 'conversation' || type === 'modal';
|
|
28
28
|
const { priceCurrency } = productCardProps || {};
|
|
@@ -36,6 +36,7 @@ function CioPiaQna(props) {
|
|
|
36
36
|
answerParameters,
|
|
37
37
|
parameters,
|
|
38
38
|
formatImageUrl: formatters === null || formatters === void 0 ? void 0 : formatters.formatImageUrl,
|
|
39
|
+
testCells: abTest === null || abTest === void 0 ? void 0 : abTest.testCells,
|
|
39
40
|
});
|
|
40
41
|
const tracking = (0, useTracking_1.default)({
|
|
41
42
|
cioClient: pia.cioClient,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const react_1 = tslib_1.__importDefault(require("react"));
|
|
5
|
+
const usePiaClient_1 = tslib_1.__importDefault(require("../../hooks/usePiaClient"));
|
|
6
|
+
const useTracking_1 = tslib_1.__importDefault(require("../../hooks/useTracking"));
|
|
7
|
+
const useViewportTracking_1 = tslib_1.__importDefault(require("../../hooks/useViewportTracking"));
|
|
8
|
+
const NO_QUESTIONS = [];
|
|
9
|
+
/**
|
|
10
|
+
* What a control-group shopper gets instead of the widget: nothing to look at, but a node the
|
|
11
|
+
* view event can be tied to, so both arms of an A/B test are measured the same way.
|
|
12
|
+
*
|
|
13
|
+
* None of the question or answer hooks mount here, so the control arm cannot issue a request.
|
|
14
|
+
*/
|
|
15
|
+
function CioPiaControl(props) {
|
|
16
|
+
const { apiKey, itemId, itemName, threadId, variationId, cioClient, abTest, trackingConfigs } = props;
|
|
17
|
+
const { cioClient: client, threadId: resolvedThreadId } = (0, usePiaClient_1.default)({
|
|
18
|
+
apiKey,
|
|
19
|
+
threadId,
|
|
20
|
+
cioClient,
|
|
21
|
+
testCells: abTest === null || abTest === void 0 ? void 0 : abTest.testCells,
|
|
22
|
+
});
|
|
23
|
+
const tracking = (0, useTracking_1.default)({
|
|
24
|
+
cioClient: client,
|
|
25
|
+
itemId,
|
|
26
|
+
itemName,
|
|
27
|
+
variationId,
|
|
28
|
+
threadId: resolvedThreadId,
|
|
29
|
+
});
|
|
30
|
+
const { containerRef } = (0, useViewportTracking_1.default)({
|
|
31
|
+
tracking,
|
|
32
|
+
questions: NO_QUESTIONS,
|
|
33
|
+
viewThreshold: trackingConfigs === null || trackingConfigs === void 0 ? void 0 : trackingConfigs.viewThreshold,
|
|
34
|
+
awaitQuestions: false,
|
|
35
|
+
});
|
|
36
|
+
return (react_1.default.createElement("div", { ref: containerRef, className: 'cio-pia-control-placeholder', "data-testid": 'cio-pia-control-placeholder', style: { width: 1, height: 1, padding: 0, border: 0, margin: 0, overflow: 'hidden' }, "aria-hidden": 'true' }));
|
|
37
|
+
}
|
|
38
|
+
exports.default = CioPiaControl;
|
|
@@ -20,8 +20,9 @@ const classNames_1 = require("../../utils/classNames");
|
|
|
20
20
|
* that supplied its own root — `children` or `componentOverrides.reactNode` — still gets it in that
|
|
21
21
|
* state, with `error` available, and decides what belongs there.
|
|
22
22
|
*/
|
|
23
|
+
// eslint-disable-next-line complexity
|
|
23
24
|
function CioPiaRecs(props) {
|
|
24
|
-
const { apiKey, itemId, threadId, variationId, cioClient, componentOverrides, callbacks, formatters, productCardProps, children, translations, recsPodParameters, } = props;
|
|
25
|
+
const { apiKey, itemId, threadId, variationId, cioClient, componentOverrides, callbacks, formatters, productCardProps, children, translations, recsPodParameters, abTest, } = props;
|
|
25
26
|
const { priceCurrency } = productCardProps || {};
|
|
26
27
|
const { showInput = true, numResults } = recsPodParameters || {};
|
|
27
28
|
const { answer: answerOverride, carousel: carouselOverride, loading: loadingOverride, reactNode: rootOverride, } = componentOverrides || {};
|
|
@@ -29,6 +30,7 @@ function CioPiaRecs(props) {
|
|
|
29
30
|
apiKey,
|
|
30
31
|
threadId,
|
|
31
32
|
cioClient,
|
|
33
|
+
testCells: abTest === null || abTest === void 0 ? void 0 : abTest.testCells,
|
|
32
34
|
});
|
|
33
35
|
const { title, items, refinement, isLoading, error, inputError, lastShopperInput, refine } = (0, useRecsPod_1.default)({
|
|
34
36
|
itemId,
|
|
@@ -40,11 +40,12 @@ function mapDeprecatedParameters(params) {
|
|
|
40
40
|
return hasEntries ? mapped : undefined;
|
|
41
41
|
}
|
|
42
42
|
function useCioPia(props) {
|
|
43
|
-
const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, answerParameters, parameters, formatImageUrl, } = props;
|
|
43
|
+
const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, answerParameters, parameters, formatImageUrl, testCells, } = props;
|
|
44
44
|
const { cioClient: client, threadId } = (0, usePiaClient_1.default)({
|
|
45
45
|
apiKey,
|
|
46
46
|
threadId: providedThreadId,
|
|
47
47
|
cioClient: providedClient,
|
|
48
|
+
testCells,
|
|
48
49
|
});
|
|
49
50
|
const mappedDeprecated = mapDeprecatedParameters(parameters);
|
|
50
51
|
const mergedSuggestedParams = mappedDeprecated || suggestedQuestionsParameters
|
|
@@ -4,18 +4,25 @@ const tslib_1 = require("tslib");
|
|
|
4
4
|
const react_1 = require("react");
|
|
5
5
|
const constructorio_client_javascript_1 = tslib_1.__importDefault(require("@constructor-io/constructorio-client-javascript"));
|
|
6
6
|
const version_1 = tslib_1.__importDefault(require("../version"));
|
|
7
|
-
function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, }) {
|
|
7
|
+
function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, testCells, }) {
|
|
8
8
|
const [generatedThreadId] = (0, react_1.useState)(() => crypto.randomUUID());
|
|
9
9
|
const threadId = providedThreadId || generatedThreadId;
|
|
10
|
+
const serializedTestCells = (0, react_1.useMemo)(() => JSON.stringify(testCells !== null && testCells !== void 0 ? testCells : null), [testCells]);
|
|
10
11
|
const client = (0, react_1.useMemo)(() => {
|
|
11
12
|
if (providedClient)
|
|
12
13
|
return providedClient;
|
|
13
|
-
return new constructorio_client_javascript_1.default({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
return new constructorio_client_javascript_1.default(Object.assign({ apiKey, sendTrackingEvents: true, version: `cio-ui-pia-${version_1.default}` }, (testCells && { testCells })));
|
|
15
|
+
// testCells is compared by serializedTestCells so an inline object does not rebuild the client
|
|
16
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
17
|
+
}, [apiKey, providedClient, serializedTestCells]);
|
|
18
|
+
// A caller's client owns its own options, so say so rather than dropping these silently.
|
|
19
|
+
(0, react_1.useEffect)(() => {
|
|
20
|
+
if (!providedClient || !testCells || Object.keys(testCells).length === 0)
|
|
21
|
+
return;
|
|
22
|
+
console.warn('[CioPia] abTest.testCells is ignored when you supply your own cioClient. Set testCells on that client instead.');
|
|
23
|
+
// serializedTestCells stands in for testCells here too
|
|
24
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
25
|
+
}, [providedClient, serializedTestCells]);
|
|
19
26
|
return { cioClient: client, threadId };
|
|
20
27
|
}
|
|
21
28
|
exports.default = usePiaClient;
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const react_1 = require("react");
|
|
4
4
|
const FLUSH_INTERVAL_MS = 5 * 60 * 1000;
|
|
5
5
|
const DEFAULT_VIEW_THRESHOLD = 0.5;
|
|
6
|
-
function useViewportTracking({ tracking, questions, viewThreshold = DEFAULT_VIEW_THRESHOLD, }) {
|
|
6
|
+
function useViewportTracking({ tracking, questions, viewThreshold = DEFAULT_VIEW_THRESHOLD, awaitQuestions = true, }) {
|
|
7
7
|
const nodeRef = (0, react_1.useRef)(null);
|
|
8
8
|
const observerRef = (0, react_1.useRef)(null);
|
|
9
9
|
const intervalRef = (0, react_1.useRef)(null);
|
|
@@ -11,10 +11,14 @@ function useViewportTracking({ tracking, questions, viewThreshold = DEFAULT_VIEW
|
|
|
11
11
|
const entryTimeRef = (0, react_1.useRef)(null);
|
|
12
12
|
const questionsRef = (0, react_1.useRef)(questions);
|
|
13
13
|
const trackingRef = (0, react_1.useRef)(tracking);
|
|
14
|
+
const awaitQuestionsRef = (0, react_1.useRef)(awaitQuestions);
|
|
15
|
+
(0, react_1.useEffect)(() => {
|
|
16
|
+
awaitQuestionsRef.current = awaitQuestions;
|
|
17
|
+
}, [awaitQuestions]);
|
|
14
18
|
(0, react_1.useEffect)(() => {
|
|
15
19
|
const questionsJustArrived = questionsRef.current.length === 0 && questions.length > 0;
|
|
16
20
|
questionsRef.current = questions;
|
|
17
|
-
if (questionsJustArrived && entryTimeRef.current) {
|
|
21
|
+
if (awaitQuestionsRef.current && questionsJustArrived && entryTimeRef.current) {
|
|
18
22
|
trackingRef.current.trackView(questions);
|
|
19
23
|
}
|
|
20
24
|
}, [questions]);
|
|
@@ -30,7 +34,8 @@ function useViewportTracking({ tracking, questions, viewThreshold = DEFAULT_VIEW
|
|
|
30
34
|
});
|
|
31
35
|
entryTimeRef.current = null;
|
|
32
36
|
}
|
|
33
|
-
|
|
37
|
+
const hasPayload = !awaitQuestionsRef.current || questionsRef.current.length > 0;
|
|
38
|
+
if (timespansRef.current.length > 0 && hasPayload) {
|
|
34
39
|
trackingRef.current.trackViews(questionsRef.current, [...timespansRef.current]);
|
|
35
40
|
timespansRef.current = [];
|
|
36
41
|
}
|
|
@@ -39,7 +44,7 @@ function useViewportTracking({ tracking, questions, viewThreshold = DEFAULT_VIEW
|
|
|
39
44
|
const observer = new IntersectionObserver(([entry]) => {
|
|
40
45
|
if (entry.isIntersecting) {
|
|
41
46
|
entryTimeRef.current = new Date().toISOString();
|
|
42
|
-
if (questionsRef.current.length > 0) {
|
|
47
|
+
if (!awaitQuestionsRef.current || questionsRef.current.length > 0) {
|
|
43
48
|
trackingRef.current.trackView(questionsRef.current);
|
|
44
49
|
}
|
|
45
50
|
}
|
package/lib/cjs/version.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import CioPiaQna from './CioPiaQna';
|
|
3
3
|
import CioPiaRecs from '../CioPiaRecs/CioPiaRecs';
|
|
4
|
+
import CioPiaControl from '../CioPiaControl/CioPiaControl';
|
|
4
5
|
export default function CioPia(props) {
|
|
5
6
|
const mode = props.displayConfigs?.mode ?? 'default';
|
|
7
|
+
// Before the mode dispatch: a control shopper gets no widget in any mode.
|
|
8
|
+
if (props.abTest?.isControl) {
|
|
9
|
+
return React.createElement(CioPiaControl, { ...props });
|
|
10
|
+
}
|
|
6
11
|
if (mode === 'recommendations') {
|
|
7
12
|
return React.createElement(CioPiaRecs, { ...props });
|
|
8
13
|
}
|
|
@@ -18,7 +18,7 @@ import StatusRegion, { answerStatusMessage } from '../StatusRegion/StatusRegion'
|
|
|
18
18
|
/** The question-and-answer experience: `mode: 'default'`, `mode: 'conversation'`, and the modal. */
|
|
19
19
|
// eslint-disable-next-line complexity
|
|
20
20
|
export default function CioPiaQna(props) {
|
|
21
|
-
const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, answerParameters, parameters, trackingConfigs, } = props;
|
|
21
|
+
const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, answerParameters, parameters, trackingConfigs, abTest, } = props;
|
|
22
22
|
const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline', showPreviousItems, disclaimerPosition = 'bottom', } = displayConfigs || {};
|
|
23
23
|
const isConversation = mode === 'conversation' || type === 'modal';
|
|
24
24
|
const { priceCurrency } = productCardProps || {};
|
|
@@ -32,6 +32,7 @@ export default function CioPiaQna(props) {
|
|
|
32
32
|
answerParameters,
|
|
33
33
|
parameters,
|
|
34
34
|
formatImageUrl: formatters?.formatImageUrl,
|
|
35
|
+
testCells: abTest?.testCells,
|
|
35
36
|
});
|
|
36
37
|
const tracking = useTracking({
|
|
37
38
|
cioClient: pia.cioClient,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import usePiaClient from '../../hooks/usePiaClient';
|
|
3
|
+
import useTracking from '../../hooks/useTracking';
|
|
4
|
+
import useViewportTracking from '../../hooks/useViewportTracking';
|
|
5
|
+
const NO_QUESTIONS = [];
|
|
6
|
+
/**
|
|
7
|
+
* What a control-group shopper gets instead of the widget: nothing to look at, but a node the
|
|
8
|
+
* view event can be tied to, so both arms of an A/B test are measured the same way.
|
|
9
|
+
*
|
|
10
|
+
* None of the question or answer hooks mount here, so the control arm cannot issue a request.
|
|
11
|
+
*/
|
|
12
|
+
export default function CioPiaControl(props) {
|
|
13
|
+
const { apiKey, itemId, itemName, threadId, variationId, cioClient, abTest, trackingConfigs } = props;
|
|
14
|
+
const { cioClient: client, threadId: resolvedThreadId } = usePiaClient({
|
|
15
|
+
apiKey,
|
|
16
|
+
threadId,
|
|
17
|
+
cioClient,
|
|
18
|
+
testCells: abTest?.testCells,
|
|
19
|
+
});
|
|
20
|
+
const tracking = useTracking({
|
|
21
|
+
cioClient: client,
|
|
22
|
+
itemId,
|
|
23
|
+
itemName,
|
|
24
|
+
variationId,
|
|
25
|
+
threadId: resolvedThreadId,
|
|
26
|
+
});
|
|
27
|
+
const { containerRef } = useViewportTracking({
|
|
28
|
+
tracking,
|
|
29
|
+
questions: NO_QUESTIONS,
|
|
30
|
+
viewThreshold: trackingConfigs?.viewThreshold,
|
|
31
|
+
awaitQuestions: false,
|
|
32
|
+
});
|
|
33
|
+
return (React.createElement("div", { ref: containerRef, className: 'cio-pia-control-placeholder', "data-testid": 'cio-pia-control-placeholder', style: { width: 1, height: 1, padding: 0, border: 0, margin: 0, overflow: 'hidden' }, "aria-hidden": 'true' }));
|
|
34
|
+
}
|
|
@@ -17,8 +17,9 @@ import { cx } from '../../utils/classNames';
|
|
|
17
17
|
* that supplied its own root — `children` or `componentOverrides.reactNode` — still gets it in that
|
|
18
18
|
* state, with `error` available, and decides what belongs there.
|
|
19
19
|
*/
|
|
20
|
+
// eslint-disable-next-line complexity
|
|
20
21
|
export default function CioPiaRecs(props) {
|
|
21
|
-
const { apiKey, itemId, threadId, variationId, cioClient, componentOverrides, callbacks, formatters, productCardProps, children, translations, recsPodParameters, } = props;
|
|
22
|
+
const { apiKey, itemId, threadId, variationId, cioClient, componentOverrides, callbacks, formatters, productCardProps, children, translations, recsPodParameters, abTest, } = props;
|
|
22
23
|
const { priceCurrency } = productCardProps || {};
|
|
23
24
|
const { showInput = true, numResults } = recsPodParameters || {};
|
|
24
25
|
const { answer: answerOverride, carousel: carouselOverride, loading: loadingOverride, reactNode: rootOverride, } = componentOverrides || {};
|
|
@@ -26,6 +27,7 @@ export default function CioPiaRecs(props) {
|
|
|
26
27
|
apiKey,
|
|
27
28
|
threadId,
|
|
28
29
|
cioClient,
|
|
30
|
+
testCells: abTest?.testCells,
|
|
29
31
|
});
|
|
30
32
|
const { title, items, refinement, isLoading, error, inputError, lastShopperInput, refine } = useRecsPod({
|
|
31
33
|
itemId,
|
|
@@ -37,11 +37,12 @@ function mapDeprecatedParameters(params) {
|
|
|
37
37
|
return hasEntries ? mapped : undefined;
|
|
38
38
|
}
|
|
39
39
|
export default function useCioPia(props) {
|
|
40
|
-
const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, answerParameters, parameters, formatImageUrl, } = props;
|
|
40
|
+
const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, answerParameters, parameters, formatImageUrl, testCells, } = props;
|
|
41
41
|
const { cioClient: client, threadId } = usePiaClient({
|
|
42
42
|
apiKey,
|
|
43
43
|
threadId: providedThreadId,
|
|
44
44
|
cioClient: providedClient,
|
|
45
|
+
testCells,
|
|
45
46
|
});
|
|
46
47
|
const mappedDeprecated = mapDeprecatedParameters(parameters);
|
|
47
48
|
const mergedSuggestedParams = mappedDeprecated || suggestedQuestionsParameters
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import ConstructorIOClient from '@constructor-io/constructorio-client-javascript';
|
|
3
3
|
import version from '../version';
|
|
4
|
-
export default function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, }) {
|
|
4
|
+
export default function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, testCells, }) {
|
|
5
5
|
const [generatedThreadId] = useState(() => crypto.randomUUID());
|
|
6
6
|
const threadId = providedThreadId || generatedThreadId;
|
|
7
|
+
const serializedTestCells = useMemo(() => JSON.stringify(testCells ?? null), [testCells]);
|
|
7
8
|
const client = useMemo(() => {
|
|
8
9
|
if (providedClient)
|
|
9
10
|
return providedClient;
|
|
@@ -11,7 +12,18 @@ export default function usePiaClient({ apiKey, threadId: providedThreadId, cioCl
|
|
|
11
12
|
apiKey,
|
|
12
13
|
sendTrackingEvents: true,
|
|
13
14
|
version: `cio-ui-pia-${version}`,
|
|
15
|
+
...(testCells && { testCells }),
|
|
14
16
|
});
|
|
15
|
-
|
|
17
|
+
// testCells is compared by serializedTestCells so an inline object does not rebuild the client
|
|
18
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
19
|
+
}, [apiKey, providedClient, serializedTestCells]);
|
|
20
|
+
// A caller's client owns its own options, so say so rather than dropping these silently.
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!providedClient || !testCells || Object.keys(testCells).length === 0)
|
|
23
|
+
return;
|
|
24
|
+
console.warn('[CioPia] abTest.testCells is ignored when you supply your own cioClient. Set testCells on that client instead.');
|
|
25
|
+
// serializedTestCells stands in for testCells here too
|
|
26
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
27
|
+
}, [providedClient, serializedTestCells]);
|
|
16
28
|
return { cioClient: client, threadId };
|
|
17
29
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
const FLUSH_INTERVAL_MS = 5 * 60 * 1000;
|
|
3
3
|
const DEFAULT_VIEW_THRESHOLD = 0.5;
|
|
4
|
-
export default function useViewportTracking({ tracking, questions, viewThreshold = DEFAULT_VIEW_THRESHOLD, }) {
|
|
4
|
+
export default function useViewportTracking({ tracking, questions, viewThreshold = DEFAULT_VIEW_THRESHOLD, awaitQuestions = true, }) {
|
|
5
5
|
const nodeRef = useRef(null);
|
|
6
6
|
const observerRef = useRef(null);
|
|
7
7
|
const intervalRef = useRef(null);
|
|
@@ -9,10 +9,14 @@ export default function useViewportTracking({ tracking, questions, viewThreshold
|
|
|
9
9
|
const entryTimeRef = useRef(null);
|
|
10
10
|
const questionsRef = useRef(questions);
|
|
11
11
|
const trackingRef = useRef(tracking);
|
|
12
|
+
const awaitQuestionsRef = useRef(awaitQuestions);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
awaitQuestionsRef.current = awaitQuestions;
|
|
15
|
+
}, [awaitQuestions]);
|
|
12
16
|
useEffect(() => {
|
|
13
17
|
const questionsJustArrived = questionsRef.current.length === 0 && questions.length > 0;
|
|
14
18
|
questionsRef.current = questions;
|
|
15
|
-
if (questionsJustArrived && entryTimeRef.current) {
|
|
19
|
+
if (awaitQuestionsRef.current && questionsJustArrived && entryTimeRef.current) {
|
|
16
20
|
trackingRef.current.trackView(questions);
|
|
17
21
|
}
|
|
18
22
|
}, [questions]);
|
|
@@ -28,7 +32,8 @@ export default function useViewportTracking({ tracking, questions, viewThreshold
|
|
|
28
32
|
});
|
|
29
33
|
entryTimeRef.current = null;
|
|
30
34
|
}
|
|
31
|
-
|
|
35
|
+
const hasPayload = !awaitQuestionsRef.current || questionsRef.current.length > 0;
|
|
36
|
+
if (timespansRef.current.length > 0 && hasPayload) {
|
|
32
37
|
trackingRef.current.trackViews(questionsRef.current, [...timespansRef.current]);
|
|
33
38
|
timespansRef.current = [];
|
|
34
39
|
}
|
|
@@ -37,7 +42,7 @@ export default function useViewportTracking({ tracking, questions, viewThreshold
|
|
|
37
42
|
const observer = new IntersectionObserver(([entry]) => {
|
|
38
43
|
if (entry.isIntersecting) {
|
|
39
44
|
entryTimeRef.current = new Date().toISOString();
|
|
40
|
-
if (questionsRef.current.length > 0) {
|
|
45
|
+
if (!awaitQuestionsRef.current || questionsRef.current.length > 0) {
|
|
41
46
|
trackingRef.current.trackView(questionsRef.current);
|
|
42
47
|
}
|
|
43
48
|
}
|
package/lib/mjs/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '2.
|
|
1
|
+
export default '2.14.0';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IncludeComponentOverrides, IncludeRenderProps } from '@constructor-io/constructorio-ui-components';
|
|
2
|
-
import { CioPiaRenderProps, CioPiaComponentOverrides, Callbacks, CioPiaDisplayConfigs, CioPiaTrackingConfigs, Translations, SuggestedQuestionsParameters, AnswerRequestParameters, RecsPodParameters, Formatters, ProductCardDisplayProps } from '../../types';
|
|
2
|
+
import { CioPiaRenderProps, CioPiaComponentOverrides, Callbacks, CioPiaAbTest, CioPiaDisplayConfigs, CioPiaTrackingConfigs, Translations, SuggestedQuestionsParameters, AnswerRequestParameters, RecsPodParameters, Formatters, ProductCardDisplayProps } from '../../types';
|
|
3
3
|
import type { CioClient } from '../../hooks/usePiaClient';
|
|
4
4
|
export interface CioPiaProps extends IncludeRenderProps<CioPiaRenderProps>, IncludeComponentOverrides<CioPiaComponentOverrides> {
|
|
5
5
|
/** Your Constructor.io API key. */
|
|
@@ -18,6 +18,8 @@ export interface CioPiaProps extends IncludeRenderProps<CioPiaRenderProps>, Incl
|
|
|
18
18
|
displayConfigs?: CioPiaDisplayConfigs;
|
|
19
19
|
/** Tracking configuration options (viewThreshold, etc.). */
|
|
20
20
|
trackingConfigs?: CioPiaTrackingConfigs;
|
|
21
|
+
/** A/B test configuration: test cells to attach to events, and the control-group flag. */
|
|
22
|
+
abTest?: CioPiaAbTest;
|
|
21
23
|
/** Callback handlers for user interactions (onQuestionSubmit, onProductCardClick, onFeedback). */
|
|
22
24
|
callbacks?: Callbacks;
|
|
23
25
|
/** Custom component overrides via reactNode or render props functions. */
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { CioPiaProps } from '../CioPia/types';
|
|
3
|
+
/**
|
|
4
|
+
* What a control-group shopper gets instead of the widget: nothing to look at, but a node the
|
|
5
|
+
* view event can be tied to, so both arms of an A/B test are measured the same way.
|
|
6
|
+
*
|
|
7
|
+
* None of the question or answer hooks mount here, so the control arm cannot issue a request.
|
|
8
|
+
*/
|
|
9
|
+
export default function CioPiaControl(props: CioPiaProps): React.JSX.Element;
|
|
@@ -32,6 +32,8 @@ export interface UseCioPiaProps {
|
|
|
32
32
|
parameters?: Record<string, string | number | boolean>;
|
|
33
33
|
/** Define outside the component or wrap with useCallback to avoid unnecessary re-renders. */
|
|
34
34
|
formatImageUrl?: Formatters['formatImageUrl'];
|
|
35
|
+
/** Test cells to attach to tracking events. Ignored when `cioClient` is supplied. */
|
|
36
|
+
testCells?: Record<string, string>;
|
|
35
37
|
}
|
|
36
38
|
export interface UseCioPiaReturn {
|
|
37
39
|
cioClient: {
|
|
@@ -4,9 +4,10 @@ export interface UsePiaClientProps {
|
|
|
4
4
|
apiKey: string;
|
|
5
5
|
threadId?: string;
|
|
6
6
|
cioClient?: CioClient;
|
|
7
|
+
testCells?: Record<string, string>;
|
|
7
8
|
}
|
|
8
9
|
export interface UsePiaClientReturn {
|
|
9
10
|
cioClient: CioClient;
|
|
10
11
|
threadId: string;
|
|
11
12
|
}
|
|
12
|
-
export default function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, }: UsePiaClientProps): UsePiaClientReturn;
|
|
13
|
+
export default function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, testCells, }: UsePiaClientProps): UsePiaClientReturn;
|
|
@@ -4,8 +4,13 @@ export interface UseViewportTrackingProps {
|
|
|
4
4
|
tracking: UseTrackingReturn;
|
|
5
5
|
questions: Question[];
|
|
6
6
|
viewThreshold?: number;
|
|
7
|
+
/**
|
|
8
|
+
* Whether an empty `questions` array means "not loaded yet". False for a control-group
|
|
9
|
+
* placeholder, where no questions are ever fetched and the view must still be recorded.
|
|
10
|
+
*/
|
|
11
|
+
awaitQuestions?: boolean;
|
|
7
12
|
}
|
|
8
13
|
export interface UseViewportTrackingReturn {
|
|
9
14
|
containerRef: (node: HTMLDivElement | null) => void;
|
|
10
15
|
}
|
|
11
|
-
export default function useViewportTracking({ tracking, questions, viewThreshold, }: UseViewportTrackingProps): UseViewportTrackingReturn;
|
|
16
|
+
export default function useViewportTracking({ tracking, questions, viewThreshold, awaitQuestions, }: UseViewportTrackingProps): UseViewportTrackingReturn;
|
package/lib/types/types.d.ts
CHANGED
|
@@ -24,6 +24,11 @@ export interface CioPiaProviderProps {
|
|
|
24
24
|
threadId?: string;
|
|
25
25
|
cioClient?: Nullable<CioClient>;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Display mode. `'recommendations'` is accepted but **not yet available**: the
|
|
29
|
+
* recommendations endpoint is absent from the client SDK, so that mode renders
|
|
30
|
+
* nothing. Use `'default'` or `'conversation'`.
|
|
31
|
+
*/
|
|
27
32
|
export type CioPiaMode = 'default' | 'conversation' | 'recommendations';
|
|
28
33
|
export type CioPiaType = 'inline' | 'modal';
|
|
29
34
|
export type DisclaimerPosition = 'top' | 'bottom';
|
|
@@ -34,6 +39,34 @@ export type CioPiaTrackingConfigs = {
|
|
|
34
39
|
*/
|
|
35
40
|
viewThreshold?: number;
|
|
36
41
|
};
|
|
42
|
+
export type CioPiaAbTest = {
|
|
43
|
+
/**
|
|
44
|
+
* Test cells to attach to PIA tracking events, as `{ [testName]: cellName }`. Each entry is
|
|
45
|
+
* sent as an `ef-<testName>` parameter. Source them however you like — Constructor's docs have
|
|
46
|
+
* the page set `window.cnstrc.testCell` to a bare cell name, so label it with your test name:
|
|
47
|
+
* `{ constructorio: window.cnstrc.testCell }`. Some integrations expose a `window.cnstrc.testCells`
|
|
48
|
+
* map instead, which you can pass straight through.
|
|
49
|
+
*
|
|
50
|
+
* Ignored when you supply your own `cioClient`: that client owns its own options, so set
|
|
51
|
+
* `testCells` there instead, as a `ConstructorIOClient` constructor option. Passing both
|
|
52
|
+
* logs a warning.
|
|
53
|
+
*
|
|
54
|
+
* Empty, non-string, or whitespace-only values are silently dropped, so a cell sourced from
|
|
55
|
+
* a global that resolves to `undefined` will simply not be sent.
|
|
56
|
+
*/
|
|
57
|
+
testCells?: Record<string, string>;
|
|
58
|
+
/**
|
|
59
|
+
* Mark this shopper as the A/B control group. Renders an invisible, tracking-only placeholder
|
|
60
|
+
* instead of the widget, so both arms of the test record a view event. Takes precedence over
|
|
61
|
+
* `displayConfigs.mode`. The placeholder renders neither `children` nor `componentOverrides`,
|
|
62
|
+
* and does not fire the `onView` / `onOutOfView` callbacks — the control arm reports through
|
|
63
|
+
* Constructor's own view event only.
|
|
64
|
+
*
|
|
65
|
+
* Required: stating the arm per shopper is the one thing only you can know, and leaving it
|
|
66
|
+
* out would silently put every shopper in the test arm with no control group at all.
|
|
67
|
+
*/
|
|
68
|
+
isControl: boolean;
|
|
69
|
+
};
|
|
37
70
|
export type CioPiaDisplayConfigs = {
|
|
38
71
|
learnMoreUrl?: string;
|
|
39
72
|
showFeedback?: boolean;
|
package/lib/types/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "2.
|
|
1
|
+
declare const _default: "2.14.0";
|
|
2
2
|
export default _default;
|
package/package.json
CHANGED