@lowdefy/helpers 6.0.0 → 7.0.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/dist/appStorageKey.js +20 -0
- package/dist/buildPagePath.js +39 -0
- package/dist/builtinMessages.js +8 -0
- package/dist/callerHash.js +28 -0
- package/dist/callerStamp.js +59 -0
- package/dist/cleanBuildArtifact.js +24 -0
- package/dist/createTraceId.js +35 -0
- package/dist/extractErrorProps.js +64 -58
- package/dist/filterElementsChain.js +68 -0
- package/dist/findInteractiveControls.js +25 -0
- package/dist/getSafeHref.js +49 -0
- package/dist/index.js +26 -1
- package/dist/isChainHeaderEnd.js +33 -0
- package/dist/isInteractiveControl.js +22 -0
- package/dist/isMountEventName.js +27 -0
- package/dist/isTraceId.js +21 -0
- package/dist/journeyTargetSelectors.js +103 -0
- package/dist/listHash.js +25 -0
- package/dist/mapPlainValues.js +51 -0
- package/dist/normalizeCaller.js +30 -0
- package/dist/pageInstanceKey.js +36 -0
- package/dist/pairTraceEvents.js +102 -0
- package/dist/parseCallerStamp.js +46 -0
- package/dist/parseElementsChain.js +58 -0
- package/dist/parsePageId.js +23 -0
- package/dist/parsePathPattern.js +88 -0
- package/dist/parseRowIndex.js +24 -0
- package/dist/projectCaughtError.js +88 -0
- package/dist/readChainValue.js +42 -0
- package/dist/rolesHash.js +34 -0
- package/dist/serializer.js +10 -22
- package/dist/sha256Hex.js +170 -0
- package/dist/stampHash.js +24 -0
- package/dist/targetFromElementsChain.js +103 -0
- package/dist/traceIdDate.js +23 -0
- package/package.json +5 -3
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ const MOUNT_EVENT_NAMES = [
|
|
16
|
+
'onInit',
|
|
17
|
+
'onInitAsync',
|
|
18
|
+
'onMount',
|
|
19
|
+
'onMountAsync'
|
|
20
|
+
];
|
|
21
|
+
// Mount-class events fire by themselves when a page or block opens, so no DOM
|
|
22
|
+
// interaction ever causes one. The pairing rule and the journey compiler both
|
|
23
|
+
// need to tell them apart, and must agree on the list.
|
|
24
|
+
function isMountEventName({ eventName }) {
|
|
25
|
+
return MOUNT_EVENT_NAMES.includes(eventName);
|
|
26
|
+
}
|
|
27
|
+
export default isMountEventName;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ const TRACE_ID_PATTERN = /^\d{8}T\d{6}Z-[a-z0-9]{6}$/;
|
|
16
|
+
// A trace id names a session or run file on disk, so this check is also the
|
|
17
|
+
// path-traversal guard for anything that builds a path from one.
|
|
18
|
+
function isTraceId(value) {
|
|
19
|
+
return typeof value === 'string' && TRACE_ID_PATTERN.test(value);
|
|
20
|
+
}
|
|
21
|
+
export default isTraceId;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ // The rules a journey target is resolved by. The journey runner resolves targets with them, and
|
|
16
|
+
// the engine's describe functions read the same rules in reverse, so the two cannot drift apart.
|
|
17
|
+
// A radio or checkbox inside a <label> is reached through the label: the label carries the
|
|
18
|
+
// option's text and is what a person clicks, while the input may have no size at all (antd
|
|
19
|
+
// Segmented hides it at zero width and height).
|
|
20
|
+
const radioOptionRule = {
|
|
21
|
+
selector: 'label',
|
|
22
|
+
has: 'input[type="radio"]'
|
|
23
|
+
};
|
|
24
|
+
const checkboxOptionRule = {
|
|
25
|
+
selector: 'label',
|
|
26
|
+
has: 'input[type="checkbox"]'
|
|
27
|
+
};
|
|
28
|
+
function selectorOf({ selector, has, not = [] }) {
|
|
29
|
+
const hasPart = has ? `:has(${has})` : '';
|
|
30
|
+
return `${selector}${hasPart}${not.map((item)=>`:not(${item})`).join('')}`;
|
|
31
|
+
}
|
|
32
|
+
const radioOption = selectorOf(radioOptionRule);
|
|
33
|
+
const checkboxOption = selectorOf(checkboxOptionRule);
|
|
34
|
+
// ARIA roles that make an element an interactive control.
|
|
35
|
+
const interactiveRoles = [
|
|
36
|
+
'button',
|
|
37
|
+
'switch',
|
|
38
|
+
'checkbox',
|
|
39
|
+
'radio',
|
|
40
|
+
'tab',
|
|
41
|
+
'menuitem'
|
|
42
|
+
];
|
|
43
|
+
// The controls a click target resolves to. Each rule is a simple selector, a descendant it must
|
|
44
|
+
// have and selectors it must not match, so the rules serve both as one selector string
|
|
45
|
+
// (Playwright, browsers) and as a matcher (isInteractiveControl) for DOMs whose selector engine
|
|
46
|
+
// gets :has() and complex :not() wrong, such as jsdom.
|
|
47
|
+
const interactiveControlRules = [
|
|
48
|
+
{
|
|
49
|
+
selector: 'button'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
selector: '[role="button"]'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
selector: 'a[href]'
|
|
56
|
+
},
|
|
57
|
+
radioOptionRule,
|
|
58
|
+
checkboxOptionRule,
|
|
59
|
+
{
|
|
60
|
+
selector: 'input:not([type="hidden"])',
|
|
61
|
+
not: [
|
|
62
|
+
'label input[type="radio"]',
|
|
63
|
+
'label input[type="checkbox"]'
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
selector: 'textarea'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
selector: 'select'
|
|
71
|
+
},
|
|
72
|
+
...interactiveRoles.filter((role)=>role !== 'button').map((role)=>({
|
|
73
|
+
selector: `[role="${role}"]`
|
|
74
|
+
}))
|
|
75
|
+
];
|
|
76
|
+
// A block that renders into a portal (Modal, Drawer) puts this class on the portal's root, so a
|
|
77
|
+
// journey target scoped to the block reaches the portal content, which is outside the block's
|
|
78
|
+
// #bl- wrapper. The class is a CSS selector-safe form of the block id.
|
|
79
|
+
function portalClass(blockId) {
|
|
80
|
+
return `ldf-portal-${String(blockId).replace(/[^\w-]/g, '_')}`;
|
|
81
|
+
}
|
|
82
|
+
const journeyTargetSelectors = {
|
|
83
|
+
// Every block renders a wrapper with id `bl-<blockId>`.
|
|
84
|
+
blockWrapperPrefix: 'bl-',
|
|
85
|
+
// ag-grid's displayed row index and a cell's column id.
|
|
86
|
+
cellAttribute: 'col-id',
|
|
87
|
+
checkboxOption,
|
|
88
|
+
// The options of a dropdown, the elements a `select` step clicks.
|
|
89
|
+
dropdownOption: '.ant-select-item-option, [role="option"]',
|
|
90
|
+
interactiveControl: interactiveControlRules.map(selectorOf).join(', '),
|
|
91
|
+
interactiveControlRules,
|
|
92
|
+
interactiveRoles,
|
|
93
|
+
// Portal layers, front-most first: an open dropdown menu covers a dialog, a dialog covers the
|
|
94
|
+
// page.
|
|
95
|
+
layers: [
|
|
96
|
+
'[role="menu"]',
|
|
97
|
+
'[role="dialog"]'
|
|
98
|
+
],
|
|
99
|
+
portalClass,
|
|
100
|
+
radioOption,
|
|
101
|
+
rowAttribute: 'row-index'
|
|
102
|
+
};
|
|
103
|
+
export default journeyTargetSelectors;
|
package/dist/listHash.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import stampHash from './stampHash.js';
|
|
16
|
+
// The stamp hash of a list whose order carries no meaning (the organization ids
|
|
17
|
+
// a person belongs to): sorted first, so the same set always hashes the same,
|
|
18
|
+
// and JSON-encoded, so no separator inside an entry can make two lists read as
|
|
19
|
+
// one.
|
|
20
|
+
function listHash({ values }) {
|
|
21
|
+
return stampHash(JSON.stringify([
|
|
22
|
+
...values
|
|
23
|
+
].sort()));
|
|
24
|
+
}
|
|
25
|
+
export default listHash;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ // type.isObject also accepts class instances, whose internals a copy would expose.
|
|
16
|
+
function isPlainObject(value) {
|
|
17
|
+
if (value === null || typeof value !== 'object') return false;
|
|
18
|
+
const proto = Object.getPrototypeOf(value);
|
|
19
|
+
return proto === Object.prototype || proto === null;
|
|
20
|
+
}
|
|
21
|
+
function mapNode(value, key, visit, copies) {
|
|
22
|
+
const visited = visit(value, key);
|
|
23
|
+
if (visited !== value) return visited;
|
|
24
|
+
if (!Array.isArray(value) && !isPlainObject(value)) return value;
|
|
25
|
+
// Reusing the copy of an object already seen keeps shared references and cycles
|
|
26
|
+
// intact instead of recursing forever.
|
|
27
|
+
if (copies.has(value)) return copies.get(value);
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
const mappedArray = [];
|
|
30
|
+
copies.set(value, mappedArray);
|
|
31
|
+
value.forEach((item)=>{
|
|
32
|
+
mappedArray.push(mapNode(item, undefined, visit, copies));
|
|
33
|
+
});
|
|
34
|
+
return mappedArray;
|
|
35
|
+
}
|
|
36
|
+
const mappedObject = {};
|
|
37
|
+
copies.set(value, mappedObject);
|
|
38
|
+
Object.entries(value).forEach(([entryKey, item])=>{
|
|
39
|
+
mappedObject[entryKey] = mapNode(item, entryKey, visit, copies);
|
|
40
|
+
});
|
|
41
|
+
return mappedObject;
|
|
42
|
+
}
|
|
43
|
+
// Copies the plain objects and arrays in a value, calling visit(value, key) on every
|
|
44
|
+
// node first - key is the object key, undefined for the root and array items. A
|
|
45
|
+
// different return replaces the node; the same value is kept, and descended into
|
|
46
|
+
// when it is a plain object or array. Class instances, Dates and Errors are kept
|
|
47
|
+
// by reference. The input is never mutated.
|
|
48
|
+
function mapPlainValues(value, visit) {
|
|
49
|
+
return mapNode(value, undefined, visit, new Map());
|
|
50
|
+
}
|
|
51
|
+
export default mapPlainValues;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { snakeCase } from 'change-case';
|
|
16
|
+
import type from './type.js';
|
|
17
|
+
// Shallow snake_cases every own top-level key of the resolved caller - the
|
|
18
|
+
// object mirror of the adapter's schema-wide fieldName derive. Values are
|
|
19
|
+
// copied by reference: the attributes and profile bags (and the roles array)
|
|
20
|
+
// ride through whole, so their inner keys - app/module-owned, not platform
|
|
21
|
+
// surface - are never transformed.
|
|
22
|
+
function normalizeCaller(user) {
|
|
23
|
+
if (type.isNone(user)) return user;
|
|
24
|
+
const normalized = {};
|
|
25
|
+
for (const [key, value] of Object.entries(user)){
|
|
26
|
+
normalized[snakeCase(key)] = value;
|
|
27
|
+
}
|
|
28
|
+
return normalized;
|
|
29
|
+
}
|
|
30
|
+
export default normalizeCaller;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import buildPagePath from './buildPagePath.js';
|
|
16
|
+
import parsePathPattern from './parsePathPattern.js';
|
|
17
|
+
import type from './type.js';
|
|
18
|
+
// The key a page instance's context and input are stored under. A page with placeholders has one
|
|
19
|
+
// instance per set of decoded values, keyed by the path the builder writes for them, so every
|
|
20
|
+
// spelling of the same values in an arriving URL gives one key. "#" cannot appear in a page id and
|
|
21
|
+
// the builder encodes it in values, so no instance key equals another page's key.
|
|
22
|
+
function pageInstanceKey({ pageId, path, pathParams }) {
|
|
23
|
+
if (type.isUndefined(path)) {
|
|
24
|
+
return `page:${pageId}`;
|
|
25
|
+
}
|
|
26
|
+
const hasPlaceholders = parsePathPattern(path).some((segment)=>type.isString(segment.name));
|
|
27
|
+
if (!hasPlaceholders) {
|
|
28
|
+
return `page:${pageId}`;
|
|
29
|
+
}
|
|
30
|
+
return `page:${pageId}#${buildPagePath({
|
|
31
|
+
pageId,
|
|
32
|
+
path,
|
|
33
|
+
pathParams
|
|
34
|
+
})}`;
|
|
35
|
+
}
|
|
36
|
+
export default pageInstanceKey;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import isMountEventName from './isMountEventName.js';
|
|
16
|
+
import type from './type.js';
|
|
17
|
+
// Absorbs event-loop and render delay between the DOM interaction and the
|
|
18
|
+
// moment the engine starts the event's actions, without reaching back to an
|
|
19
|
+
// unrelated earlier click.
|
|
20
|
+
const PAIRING_SLACK_MS = 1000;
|
|
21
|
+
function canBeCaused({ event }) {
|
|
22
|
+
return event.scope !== 'app' && !isMountEventName({
|
|
23
|
+
eventName: event.eventName
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
// An interaction can have caused an event when the event's block encloses the
|
|
27
|
+
// interaction's target (the handler is on the clicked block or an ancestor of
|
|
28
|
+
// it) and the event started no later than its debounce plus the slack after
|
|
29
|
+
// the interaction.
|
|
30
|
+
function couldCause({ event, interaction }) {
|
|
31
|
+
if (!interaction.blockIds.includes(event.blockId)) return false;
|
|
32
|
+
const delay = event.startTimestamp - interaction.t;
|
|
33
|
+
const windowMs = (event.debounceMs ?? 0) + PAIRING_SLACK_MS;
|
|
34
|
+
return delay >= 0 && delay <= windowMs;
|
|
35
|
+
}
|
|
36
|
+
function findCause({ event, interactions }) {
|
|
37
|
+
let cause;
|
|
38
|
+
interactions.forEach((interaction)=>{
|
|
39
|
+
if (!couldCause({
|
|
40
|
+
event,
|
|
41
|
+
interaction
|
|
42
|
+
})) return;
|
|
43
|
+
if (type.isUndefined(cause) || interaction.t >= cause.t) {
|
|
44
|
+
cause = interaction;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return cause;
|
|
48
|
+
}
|
|
49
|
+
// Decides which DOM interaction caused which engine event, so a trace record
|
|
50
|
+
// means the same thing whichever source wrote it: the dev recorder runs this in
|
|
51
|
+
// the browser, the production pull in Node.
|
|
52
|
+
//
|
|
53
|
+
// - interactions: [{ id, t, blockIds }], `t` epoch ms, `blockIds` innermost first.
|
|
54
|
+
// - events: [{ id, blockId, eventName, startTimestamp, debounceMs, scope }].
|
|
55
|
+
//
|
|
56
|
+
// Mount-class and app events are never paired. Any other event pairs with the
|
|
57
|
+
// latest interaction that could have caused it; an event with none is
|
|
58
|
+
// programmatic and goes to `unpaired`. Within a pair, events on the innermost
|
|
59
|
+
// block come first, then the earliest, so the record builder takes the first
|
|
60
|
+
// as `event` and the rest as `also`.
|
|
61
|
+
function pairTraceEvents({ interactions, events }) {
|
|
62
|
+
const byInteraction = new Map();
|
|
63
|
+
const unpaired = [];
|
|
64
|
+
events.forEach((event, order)=>{
|
|
65
|
+
const cause = canBeCaused({
|
|
66
|
+
event
|
|
67
|
+
}) ? findCause({
|
|
68
|
+
event,
|
|
69
|
+
interactions
|
|
70
|
+
}) : undefined;
|
|
71
|
+
if (type.isUndefined(cause)) {
|
|
72
|
+
unpaired.push(event.id);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!byInteraction.has(cause)) byInteraction.set(cause, []);
|
|
76
|
+
byInteraction.get(cause).push({
|
|
77
|
+
depth: cause.blockIds.indexOf(event.blockId),
|
|
78
|
+
event,
|
|
79
|
+
order
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
const pairs = interactions.filter((interaction)=>byInteraction.has(interaction)).map((interaction)=>{
|
|
83
|
+
const paired = [
|
|
84
|
+
...byInteraction.get(interaction)
|
|
85
|
+
].sort((a, b)=>{
|
|
86
|
+
if (a.depth !== b.depth) return a.depth - b.depth;
|
|
87
|
+
if (a.event.startTimestamp !== b.event.startTimestamp) {
|
|
88
|
+
return a.event.startTimestamp - b.event.startTimestamp;
|
|
89
|
+
}
|
|
90
|
+
return a.order - b.order;
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
interactionId: interaction.id,
|
|
94
|
+
eventIds: paired.map((entry)=>entry.event.id)
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
pairs,
|
|
99
|
+
unpaired
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export default pairTraceEvents;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import type from './type.js';
|
|
16
|
+
const FIELDS = [
|
|
17
|
+
'session',
|
|
18
|
+
'roles',
|
|
19
|
+
'memberships'
|
|
20
|
+
];
|
|
21
|
+
// Reads an x-lowdefy-caller header (see callerStamp) into
|
|
22
|
+
// { caller, session, roles, memberships }. A field the header does not carry is
|
|
23
|
+
// null; an anonymous caller is { caller: '' } with every field null. No header
|
|
24
|
+
// is null: the response was not answered by a route that resolved a caller (it
|
|
25
|
+
// failed before, or came from a proxy), so it claims no caller.
|
|
26
|
+
function parseCallerStamp(header) {
|
|
27
|
+
if (type.isNone(header)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const [caller, ...fields] = header.split(';').map((part)=>part.trim());
|
|
31
|
+
const stamp = {
|
|
32
|
+
caller,
|
|
33
|
+
session: null,
|
|
34
|
+
roles: null,
|
|
35
|
+
memberships: null
|
|
36
|
+
};
|
|
37
|
+
fields.forEach((field)=>{
|
|
38
|
+
const separator = field.indexOf('=');
|
|
39
|
+
const name = field.slice(0, separator);
|
|
40
|
+
if (separator > 0 && FIELDS.includes(name)) {
|
|
41
|
+
stamp[name] = field.slice(separator + 1);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return stamp;
|
|
45
|
+
}
|
|
46
|
+
export default parseCallerStamp;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import isChainHeaderEnd from './isChainHeaderEnd.js';
|
|
16
|
+
import readChainValue from './readChainValue.js';
|
|
17
|
+
function parseHeader(header) {
|
|
18
|
+
const [tag, ...classes] = header.split('.');
|
|
19
|
+
return {
|
|
20
|
+
tag: tag.toLowerCase(),
|
|
21
|
+
classes: classes.filter((name)=>name !== '')
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// Splits a posthog-js `$elements_chain` into entries, target first. Each entry is
|
|
25
|
+
// `tag.class1.class2:` followed by sorted `key="value"` pairs, and entries are joined by `;`.
|
|
26
|
+
// Total: a malformed chain gives the entries read before the fault.
|
|
27
|
+
function parseElementsChain(chain) {
|
|
28
|
+
const entries = [];
|
|
29
|
+
let index = 0;
|
|
30
|
+
while(index < chain.length){
|
|
31
|
+
let headerEnd = index;
|
|
32
|
+
while(headerEnd < chain.length && chain[headerEnd] !== ';' && !isChainHeaderEnd(chain, headerEnd)){
|
|
33
|
+
headerEnd += 1;
|
|
34
|
+
}
|
|
35
|
+
const entry = {
|
|
36
|
+
...parseHeader(chain.slice(index, headerEnd)),
|
|
37
|
+
attributes: {}
|
|
38
|
+
};
|
|
39
|
+
entries.push(entry);
|
|
40
|
+
if (chain[headerEnd] !== ':') {
|
|
41
|
+
index = headerEnd + 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
index = headerEnd + 1;
|
|
45
|
+
while(index < chain.length && chain[index] !== ';'){
|
|
46
|
+
const keyEnd = chain.indexOf('="', index);
|
|
47
|
+
if (keyEnd === -1) {
|
|
48
|
+
return entries;
|
|
49
|
+
}
|
|
50
|
+
const { value, end } = readChainValue(chain, keyEnd + 2);
|
|
51
|
+
entry.attributes[chain.slice(index, keyEnd)] = value;
|
|
52
|
+
index = end;
|
|
53
|
+
}
|
|
54
|
+
index += 1;
|
|
55
|
+
}
|
|
56
|
+
return entries;
|
|
57
|
+
}
|
|
58
|
+
export default parseElementsChain;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ // The page path a URL shows: its pathname without the basePath and the outer slashes. It is the
|
|
16
|
+
// page id only for pages without a `path` pattern. The app root gives null.
|
|
17
|
+
function parsePageId(url, basePath = '') {
|
|
18
|
+
const pathname = new URL(url, 'http://localhost').pathname;
|
|
19
|
+
const stripped = basePath && pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
|
|
20
|
+
const pageId = stripped.replace(/^\//, '').replace(/\/$/, '');
|
|
21
|
+
return pageId === '' ? null : pageId;
|
|
22
|
+
}
|
|
23
|
+
export default parsePageId;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import type from './type.js';
|
|
16
|
+
// The page id character set without "/", which separates segments.
|
|
17
|
+
const fixedSegmentPattern = /^[A-Za-z0-9\-_:]+$/;
|
|
18
|
+
const placeholderNamePattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
19
|
+
function parsePlaceholder({ path, segment }) {
|
|
20
|
+
const name = segment.slice(1, -1);
|
|
21
|
+
if (name.startsWith('...')) {
|
|
22
|
+
throw new Error(`Page path "${path}" has a catch-all placeholder "${segment}". Catch-all placeholders are not supported.`);
|
|
23
|
+
}
|
|
24
|
+
if (name.startsWith('?') || name.endsWith('?')) {
|
|
25
|
+
throw new Error(`Page path "${path}" has an optional placeholder "${segment}". Optional placeholders are not supported.`);
|
|
26
|
+
}
|
|
27
|
+
if (!placeholderNamePattern.test(name)) {
|
|
28
|
+
throw new Error(`Page path "${path}" has an invalid placeholder "${segment}". Placeholder names must start with a letter or "_" and only contain A-Z, a-z, 0-9 and "_".`);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
name
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function parseSegment({ path, segment }) {
|
|
35
|
+
if (segment === '') {
|
|
36
|
+
throw new Error(`Page path "${path}" has an empty segment.`);
|
|
37
|
+
}
|
|
38
|
+
if (segment.startsWith('{') && segment.endsWith('}')) {
|
|
39
|
+
return parsePlaceholder({
|
|
40
|
+
path,
|
|
41
|
+
segment
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (segment.includes('{') || segment.includes('}')) {
|
|
45
|
+
throw new Error(`Page path "${path}" has a placeholder inside segment "${segment}". A placeholder must be a whole segment, like "{id}".`);
|
|
46
|
+
}
|
|
47
|
+
if (!fixedSegmentPattern.test(segment)) {
|
|
48
|
+
throw new Error(`Page path "${path}" segment "${segment}" contains invalid characters. Fixed segments must only contain A-Z, a-z, 0-9, "-", "_" and ":".`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
fixed: segment
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function parseSegments(path) {
|
|
55
|
+
if (path.startsWith('/') || path.endsWith('/')) {
|
|
56
|
+
throw new Error(`Page path "${path}" must not start or end with "/".`);
|
|
57
|
+
}
|
|
58
|
+
const names = new Set();
|
|
59
|
+
return path.split('/').map((segment)=>{
|
|
60
|
+
const parsed = parseSegment({
|
|
61
|
+
path,
|
|
62
|
+
segment
|
|
63
|
+
});
|
|
64
|
+
if (type.isString(parsed.name)) {
|
|
65
|
+
if (names.has(parsed.name)) {
|
|
66
|
+
throw new Error(`Page path "${path}" uses placeholder "${segment}" more than once.`);
|
|
67
|
+
}
|
|
68
|
+
names.add(parsed.name);
|
|
69
|
+
}
|
|
70
|
+
return Object.freeze(parsed);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Patterns come from build config, so the cache holds at most one entry per page path.
|
|
74
|
+
const cache = new Map();
|
|
75
|
+
// Splits a page path pattern into segments, each { fixed } or { name }. The result is shared
|
|
76
|
+
// between callers and frozen.
|
|
77
|
+
function parsePathPattern(path) {
|
|
78
|
+
if (!type.isString(path)) {
|
|
79
|
+
throw new Error(`Page path must be a string. Received ${JSON.stringify(path)}.`);
|
|
80
|
+
}
|
|
81
|
+
if (cache.has(path)) {
|
|
82
|
+
return cache.get(path);
|
|
83
|
+
}
|
|
84
|
+
const segments = Object.freeze(parseSegments(path));
|
|
85
|
+
cache.set(path, segments);
|
|
86
|
+
return segments;
|
|
87
|
+
}
|
|
88
|
+
export default parsePathPattern;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import type from './type.js';
|
|
16
|
+
// An ag-grid `row-index` as a journey target row. Pinned rows (`t-0`, `b-0`) are not zero-based
|
|
17
|
+
// integers, so they give null.
|
|
18
|
+
function parseRowIndex(value) {
|
|
19
|
+
if (!type.isString(value) || !/^\d+$/.test(value)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return Number(value);
|
|
23
|
+
}
|
|
24
|
+
export default parseRowIndex;
|