@drip-apex/sdk 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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/_headers +3 -0
- package/dist/bundle-report.json +564 -0
- package/dist/drip.debug.js +2 -0
- package/dist/drip.debug.js.map +1 -0
- package/dist/drip.heatmap.js +2 -0
- package/dist/drip.heatmap.js.map +1 -0
- package/dist/drip.js +2 -0
- package/dist/drip.js.map +1 -0
- package/dist/drip.live.js +2 -0
- package/dist/drip.live.js.map +1 -0
- package/dist/editor.js +480 -0
- package/dist/editor.js.map +7 -0
- package/dist/index.mjs +645 -0
- package/dist/index.mjs.map +7 -0
- package/dist/metafiles/drip.debug.meta.json +973 -0
- package/dist/metafiles/drip.heatmap.meta.json +918 -0
- package/dist/metafiles/drip.live.meta.json +918 -0
- package/dist/metafiles/drip.meta.json +973 -0
- package/dist/metafiles/editor.meta.json +321 -0
- package/dist/metafiles/index.meta.json +1039 -0
- package/dist/metafiles/index.mjs.meta.json +78 -0
- package/dist/react/index.js +16973 -0
- package/dist/react/index.js.map +7 -0
- package/dist/sri.json +27 -0
- package/dist/types/analytics.d.ts +15 -0
- package/dist/types/antiflicker.d.ts +14 -0
- package/dist/types/assignment-store.d.ts +30 -0
- package/dist/types/audit.d.ts +9 -0
- package/dist/types/bucket.d.ts +2 -0
- package/dist/types/bucketing-cleanup.d.ts +5 -0
- package/dist/types/commerce-gift.d.ts +41 -0
- package/dist/types/commerce-price.d.ts +22 -0
- package/dist/types/commerce-shipping.d.ts +28 -0
- package/dist/types/css.d.ts +5 -0
- package/dist/types/cwv.d.ts +10 -0
- package/dist/types/data-layer-normalize.d.ts +1 -0
- package/dist/types/debug.d.ts +23 -0
- package/dist/types/define.d.ts +87 -0
- package/dist/types/devtools.d.ts +32 -0
- package/dist/types/dom-trigger.d.ts +3 -0
- package/dist/types/editor/api.d.ts +37 -0
- package/dist/types/editor/artifacts.d.ts +15 -0
- package/dist/types/editor/command-runtime.d.ts +48 -0
- package/dist/types/editor/controller.d.ts +97 -0
- package/dist/types/editor/device-preview.d.ts +23 -0
- package/dist/types/editor/index.d.ts +9 -0
- package/dist/types/editor/operator.d.ts +39 -0
- package/dist/types/editor/picker.d.ts +23 -0
- package/dist/types/editor/selector.d.ts +2 -0
- package/dist/types/editor/types.d.ts +55 -0
- package/dist/types/editor/view-kit.d.ts +33 -0
- package/dist/types/editor/view.d.ts +5 -0
- package/dist/types/editor-bootstrap.d.ts +37 -0
- package/dist/types/exclusion-groups.d.ts +59 -0
- package/dist/types/exposure-signals.d.ts +7 -0
- package/dist/types/flags.d.ts +65 -0
- package/dist/types/goals.d.ts +16 -0
- package/dist/types/guards.d.ts +39 -0
- package/dist/types/handover.d.ts +10 -0
- package/dist/types/hash.d.ts +19 -0
- package/dist/types/heatmap.d.ts +8 -0
- package/dist/types/hideflicker.d.ts +15 -0
- package/dist/types/hydration.d.ts +11 -0
- package/dist/types/index.d.ts +144 -0
- package/dist/types/js.d.ts +6 -0
- package/dist/types/module.d.ts +4 -0
- package/dist/types/money.d.ts +14 -0
- package/dist/types/mutate.d.ts +34 -0
- package/dist/types/observer-mute.d.ts +3 -0
- package/dist/types/pii-keys.d.ts +1 -0
- package/dist/types/prerequisites.d.ts +12 -0
- package/dist/types/qa-assistant-state.d.ts +68 -0
- package/dist/types/qa-assistant-view.d.ts +6 -0
- package/dist/types/qa-assistant.d.ts +9 -0
- package/dist/types/qa-toolbar.d.ts +19 -0
- package/dist/types/react/index.d.ts +84 -0
- package/dist/types/reassert-sentinel.d.ts +6 -0
- package/dist/types/router.d.ts +9 -0
- package/dist/types/runtime-tools.d.ts +50 -0
- package/dist/types/runtime.d.ts +22 -0
- package/dist/types/safe-regex.d.ts +6 -0
- package/dist/types/sanitize.d.ts +5 -0
- package/dist/types/semantic.d.ts +7 -0
- package/dist/types/shopify-cart-events.d.ts +7 -0
- package/dist/types/shopify-cart.d.ts +62 -0
- package/dist/types/signals.d.ts +31 -0
- package/dist/types/surface-signals.d.ts +6 -0
- package/dist/types/targeting-diagnostics.d.ts +5 -0
- package/dist/types/targeting.d.ts +50 -0
- package/dist/types/test-fixtures/commerce.d.ts +12 -0
- package/dist/types/track.d.ts +79 -0
- package/dist/types/types.d.ts +859 -0
- package/dist/types/wait.d.ts +7 -0
- package/package.json +74 -0
- package/src/analytics.ts +146 -0
- package/src/antiflicker.test.ts +127 -0
- package/src/antiflicker.ts +147 -0
- package/src/assignment-store.test.ts +171 -0
- package/src/assignment-store.ts +208 -0
- package/src/audit.test.ts +17 -0
- package/src/audit.ts +362 -0
- package/src/bucket.test.ts +65 -0
- package/src/bucket.ts +18 -0
- package/src/bucketing-cleanup.test.ts +30 -0
- package/src/bucketing-cleanup.ts +72 -0
- package/src/bucketing-parity.test.ts +41 -0
- package/src/commerce-analytics-lifecycle.test.ts +1416 -0
- package/src/commerce-config-lifecycle.test.ts +274 -0
- package/src/commerce-epoch-lifecycle.test.ts +62 -0
- package/src/commerce-gift.test.ts +1293 -0
- package/src/commerce-gift.ts +979 -0
- package/src/commerce-offers-display.test.ts +154 -0
- package/src/commerce-price.test.ts +1145 -0
- package/src/commerce-price.ts +941 -0
- package/src/commerce-revocation-lifecycle.test.ts +78 -0
- package/src/commerce-shipping.test.ts +630 -0
- package/src/commerce-shipping.ts +447 -0
- package/src/config-fetch.test.ts +325 -0
- package/src/css.ts +54 -0
- package/src/cwv.test.ts +95 -0
- package/src/cwv.ts +129 -0
- package/src/data-layer-normalize.ts +21 -0
- package/src/debug.test.ts +83 -0
- package/src/debug.ts +138 -0
- package/src/define.test.ts +327 -0
- package/src/define.ts +208 -0
- package/src/devtools.ts +582 -0
- package/src/dom-trigger.test.ts +70 -0
- package/src/dom-trigger.ts +55 -0
- package/src/editor/api.test.ts +154 -0
- package/src/editor/api.ts +185 -0
- package/src/editor/artifacts.test.ts +57 -0
- package/src/editor/artifacts.ts +115 -0
- package/src/editor/command-runtime.test.ts +225 -0
- package/src/editor/command-runtime.ts +572 -0
- package/src/editor/controller.test.ts +371 -0
- package/src/editor/controller.ts +596 -0
- package/src/editor/device-preview.test.ts +60 -0
- package/src/editor/device-preview.ts +120 -0
- package/src/editor/index.test.ts +161 -0
- package/src/editor/index.ts +169 -0
- package/src/editor/operator.test.ts +148 -0
- package/src/editor/operator.ts +256 -0
- package/src/editor/picker.test.ts +58 -0
- package/src/editor/picker.ts +152 -0
- package/src/editor/selector.test.ts +44 -0
- package/src/editor/selector.ts +110 -0
- package/src/editor/types.ts +92 -0
- package/src/editor/view-kit.ts +162 -0
- package/src/editor/view.test.ts +535 -0
- package/src/editor/view.ts +1395 -0
- package/src/editor-bootstrap.test.ts +116 -0
- package/src/editor-bootstrap.ts +213 -0
- package/src/event-schema-contract.test.ts +35 -0
- package/src/exclusion-groups.test.ts +383 -0
- package/src/exclusion-groups.ts +270 -0
- package/src/exposure-signals.test.ts +182 -0
- package/src/exposure-signals.ts +110 -0
- package/src/flags.test.ts +322 -0
- package/src/flags.ts +201 -0
- package/src/goals.test.ts +42 -0
- package/src/goals.ts +200 -0
- package/src/guards.test.ts +249 -0
- package/src/guards.ts +250 -0
- package/src/handover-cache-order.test.ts +32 -0
- package/src/handover.test.ts +349 -0
- package/src/handover.ts +309 -0
- package/src/hash.ts +214 -0
- package/src/heatmap.test.ts +305 -0
- package/src/heatmap.ts +373 -0
- package/src/hideflicker.ts +21 -0
- package/src/hydration.test.ts +252 -0
- package/src/hydration.ts +203 -0
- package/src/index.ts +7513 -0
- package/src/init-empty.test.ts +5572 -0
- package/src/js.test.ts +26 -0
- package/src/js.ts +61 -0
- package/src/module.ts +50 -0
- package/src/money.test.ts +35 -0
- package/src/money.ts +53 -0
- package/src/mutate.test.ts +304 -0
- package/src/mutate.ts +469 -0
- package/src/observer-mute.test.ts +88 -0
- package/src/observer-mute.ts +45 -0
- package/src/page-trigger.test.ts +84 -0
- package/src/personalization.test.ts +68 -0
- package/src/pii-keys.ts +17 -0
- package/src/prerequisites.test.ts +39 -0
- package/src/prerequisites.ts +94 -0
- package/src/qa-assistant-state.test.ts +471 -0
- package/src/qa-assistant-state.ts +505 -0
- package/src/qa-assistant-view.test.ts +185 -0
- package/src/qa-assistant-view.ts +768 -0
- package/src/qa-assistant.ts +40 -0
- package/src/qa-toolbar.ts +444 -0
- package/src/react/index.test.tsx +177 -0
- package/src/react/index.tsx +252 -0
- package/src/reassert-sentinel.test.ts +314 -0
- package/src/reassert-sentinel.ts +191 -0
- package/src/router.test.ts +78 -0
- package/src/router.ts +92 -0
- package/src/runtime-tools.test.ts +112 -0
- package/src/runtime-tools.ts +403 -0
- package/src/runtime.test.ts +217 -0
- package/src/runtime.ts +268 -0
- package/src/safe-regex.ts +35 -0
- package/src/sanitize.ts +34 -0
- package/src/semantic.ts +245 -0
- package/src/shopify-cart-events.test.ts +76 -0
- package/src/shopify-cart-events.ts +115 -0
- package/src/shopify-cart.test.ts +2823 -0
- package/src/shopify-cart.ts +1256 -0
- package/src/signals.ts +226 -0
- package/src/surface-signals.test.ts +83 -0
- package/src/surface-signals.ts +165 -0
- package/src/targeting-diagnostics.ts +560 -0
- package/src/targeting.detailed.test.ts +443 -0
- package/src/targeting.test.ts +1054 -0
- package/src/targeting.ts +1024 -0
- package/src/test-fixtures/commerce.ts +65 -0
- package/src/track.test.ts +817 -0
- package/src/track.ts +519 -0
- package/src/types.ts +991 -0
- package/src/wait.test.ts +96 -0
- package/src/wait.ts +238 -0
package/src/devtools.ts
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drip SDK Devtools Overlay
|
|
3
|
+
*
|
|
4
|
+
* Enable via: `?drip_devtools=1`, `?drip_debug=1`, or `localStorage.drip_devtools=1`
|
|
5
|
+
*
|
|
6
|
+
* Renders a draggable panel (shadow DOM) showing:
|
|
7
|
+
* - Active experiments and assignments
|
|
8
|
+
* - Targeting evaluation results
|
|
9
|
+
* - Event log (assignments, mutations, goals, tracking)
|
|
10
|
+
* - Force variation controls
|
|
11
|
+
* - Performance timings
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ActivationGateDiagnostic, Assignment, Experiment, SdkDiagnostics } from "./types";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Event log
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface DevtoolsEvent {
|
|
21
|
+
ts: number;
|
|
22
|
+
type: string;
|
|
23
|
+
label: string;
|
|
24
|
+
detail?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const eventLog: DevtoolsEvent[] = [];
|
|
28
|
+
const MAX_LOG_ENTRIES = 200;
|
|
29
|
+
const eventListeners = new Set<(event: DevtoolsEvent) => void>();
|
|
30
|
+
|
|
31
|
+
export function logDevtoolsEvent(
|
|
32
|
+
type: string,
|
|
33
|
+
label: string,
|
|
34
|
+
detail?: Record<string, unknown>,
|
|
35
|
+
): void {
|
|
36
|
+
const event = { ts: Date.now(), type, label, detail };
|
|
37
|
+
eventLog.push(event);
|
|
38
|
+
if (eventLog.length > MAX_LOG_ENTRIES) eventLog.shift();
|
|
39
|
+
for (const listener of eventListeners) {
|
|
40
|
+
try {
|
|
41
|
+
listener(event);
|
|
42
|
+
} catch {
|
|
43
|
+
// Devtools listeners must not affect SDK runtime behavior.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Notify mounted devtools
|
|
47
|
+
if (panelInstance) panelInstance.refresh();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function subscribeDevtoolsEvents(listener: (event: DevtoolsEvent) => void): () => void {
|
|
51
|
+
eventListeners.add(listener);
|
|
52
|
+
return () => {
|
|
53
|
+
eventListeners.delete(listener);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Detection
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
export function isDevtoolsEnabled(): boolean {
|
|
62
|
+
try {
|
|
63
|
+
if (typeof URLSearchParams !== "undefined") {
|
|
64
|
+
const params = new URLSearchParams(location.search);
|
|
65
|
+
if (params.get("drip_devtools") === "1") return true;
|
|
66
|
+
if (params.get("drip_debug") === "1") return true;
|
|
67
|
+
if (params.get("apex_debug") === "1") return true;
|
|
68
|
+
}
|
|
69
|
+
if (typeof localStorage !== "undefined") {
|
|
70
|
+
return localStorage.getItem("drip_devtools") === "1";
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Panel instance
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
let panelInstance: DevtoolsPanel | null = null;
|
|
83
|
+
|
|
84
|
+
interface MountOptions {
|
|
85
|
+
experiments: Experiment[];
|
|
86
|
+
assignments: Assignment[];
|
|
87
|
+
diagnostics?: SdkDiagnostics | null;
|
|
88
|
+
forceMap: Record<string, string> | null;
|
|
89
|
+
onForceVariation: (experimentId: string, variationId: string) => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function mountDevtools(opts: MountOptions): void {
|
|
93
|
+
if (panelInstance) {
|
|
94
|
+
panelInstance.update(opts);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
panelInstance = new DevtoolsPanel(opts);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function unmountDevtools(): void {
|
|
101
|
+
if (panelInstance) {
|
|
102
|
+
panelInstance.destroy();
|
|
103
|
+
panelInstance = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Panel implementation
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
class DevtoolsPanel {
|
|
112
|
+
private host: HTMLElement;
|
|
113
|
+
private shadow: ShadowRoot;
|
|
114
|
+
private opts: MountOptions;
|
|
115
|
+
private collapsed = false;
|
|
116
|
+
private activeTab: "sequence" | "assignments" | "log" | "force" = "sequence";
|
|
117
|
+
private dragState: { startX: number; startY: number; offsetX: number; offsetY: number } | null = null;
|
|
118
|
+
|
|
119
|
+
constructor(opts: MountOptions) {
|
|
120
|
+
this.opts = opts;
|
|
121
|
+
this.host = document.createElement("div");
|
|
122
|
+
this.host.id = "drip-devtools-host";
|
|
123
|
+
this.shadow = this.host.attachShadow({ mode: "closed" });
|
|
124
|
+
document.body.appendChild(this.host);
|
|
125
|
+
this.render();
|
|
126
|
+
this.attachDrag();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
update(opts: MountOptions): void {
|
|
130
|
+
this.opts = opts;
|
|
131
|
+
this.render();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
refresh(): void {
|
|
135
|
+
this.render();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
destroy(): void {
|
|
139
|
+
this.host.remove();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private render(): void {
|
|
143
|
+
const { experiments, assignments, diagnostics, forceMap } = this.opts;
|
|
144
|
+
|
|
145
|
+
this.shadow.innerHTML = `
|
|
146
|
+
<style>${PANEL_CSS}</style>
|
|
147
|
+
<div class="dt-panel" id="dt-panel">
|
|
148
|
+
<div class="dt-header" id="dt-header">
|
|
149
|
+
<span class="dt-title">Drip Devtools</span>
|
|
150
|
+
<span class="dt-badge">${assignments.length} active</span>
|
|
151
|
+
<button class="dt-btn dt-toggle" id="dt-toggle">${this.collapsed ? "+" : "−"}</button>
|
|
152
|
+
</div>
|
|
153
|
+
${this.collapsed ? "" : `
|
|
154
|
+
<div class="dt-tabs">
|
|
155
|
+
<button class="dt-tab ${this.activeTab === "sequence" ? "active" : ""}" data-tab="sequence">Sequence</button>
|
|
156
|
+
<button class="dt-tab ${this.activeTab === "assignments" ? "active" : ""}" data-tab="assignments">Assignments</button>
|
|
157
|
+
<button class="dt-tab ${this.activeTab === "log" ? "active" : ""}" data-tab="log">Log (${eventLog.length})</button>
|
|
158
|
+
<button class="dt-tab ${this.activeTab === "force" ? "active" : ""}" data-tab="force">Force</button>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="dt-body">
|
|
161
|
+
${this.renderTabContent(experiments, assignments, diagnostics ?? null, forceMap)}
|
|
162
|
+
</div>
|
|
163
|
+
`}
|
|
164
|
+
</div>
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
// Bind events
|
|
168
|
+
this.shadow.getElementById("dt-toggle")?.addEventListener("click", () => {
|
|
169
|
+
this.collapsed = !this.collapsed;
|
|
170
|
+
this.render();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
this.shadow.querySelectorAll(".dt-tab").forEach((tab) => {
|
|
174
|
+
tab.addEventListener("click", () => {
|
|
175
|
+
this.activeTab = (tab as HTMLElement).dataset.tab as typeof this.activeTab;
|
|
176
|
+
this.render();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Force variation selects
|
|
181
|
+
this.shadow.querySelectorAll<HTMLSelectElement>(".dt-force-select").forEach((select) => {
|
|
182
|
+
select.addEventListener("change", () => {
|
|
183
|
+
const expId = select.dataset.expId!;
|
|
184
|
+
const varId = select.value;
|
|
185
|
+
if (varId) this.opts.onForceVariation(expId, varId);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private renderTabContent(
|
|
191
|
+
experiments: Experiment[],
|
|
192
|
+
assignments: Assignment[],
|
|
193
|
+
diagnostics: SdkDiagnostics | null,
|
|
194
|
+
forceMap: Record<string, string> | null,
|
|
195
|
+
): string {
|
|
196
|
+
switch (this.activeTab) {
|
|
197
|
+
case "sequence":
|
|
198
|
+
return this.renderSequence(diagnostics);
|
|
199
|
+
case "assignments":
|
|
200
|
+
return this.renderAssignments(assignments);
|
|
201
|
+
case "log":
|
|
202
|
+
return this.renderLog();
|
|
203
|
+
case "force":
|
|
204
|
+
return this.renderForce(experiments, forceMap);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private renderSequence(diagnostics: SdkDiagnostics | null): string {
|
|
209
|
+
if (!diagnostics) return `<div class="dt-empty">No diagnostics available</div>`;
|
|
210
|
+
const project = diagnostics.activation.project.gates;
|
|
211
|
+
const experiments = diagnostics.activation.experiments;
|
|
212
|
+
return `
|
|
213
|
+
<div class="dt-section-title">Project</div>
|
|
214
|
+
<div class="dt-card">${project.map((gate) => this.renderGate(gate)).join("")}</div>
|
|
215
|
+
<div class="dt-section-title">Experiments</div>
|
|
216
|
+
${experiments.length === 0
|
|
217
|
+
? `<div class="dt-empty compact">No experiment sequence yet</div>`
|
|
218
|
+
: experiments.map((experiment) => `
|
|
219
|
+
<div class="dt-card">
|
|
220
|
+
<div class="dt-card-title">${esc(experiment.experimentName || experiment.experimentId)}</div>
|
|
221
|
+
<div class="dt-card-row"><span>Status:</span> <span class="dt-status dt-status-${experiment.status}">${esc(experiment.status)}</span></div>
|
|
222
|
+
${experiment.assignedVariationId
|
|
223
|
+
? `<div class="dt-card-row"><span>Variation:</span> <span>${esc(experiment.assignedVariationId)}</span></div>`
|
|
224
|
+
: `<div class="dt-card-row"><span>Reason:</span> <span>${esc(experiment.reason || "not_assigned")}</span></div>`}
|
|
225
|
+
<div class="dt-gates">${experiment.gates.map((gate) => this.renderGate(gate)).join("")}</div>
|
|
226
|
+
</div>
|
|
227
|
+
`).join("")}
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private renderGate(gate: ActivationGateDiagnostic): string {
|
|
232
|
+
return `
|
|
233
|
+
<div class="dt-gate">
|
|
234
|
+
<span class="dt-dot dt-dot-${gate.status}"></span>
|
|
235
|
+
<span class="dt-gate-label">${esc(gate.label)}</span>
|
|
236
|
+
<span class="dt-gate-reason">${esc(gate.reason || gate.status)}</span>
|
|
237
|
+
</div>
|
|
238
|
+
`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private renderAssignments(assignments: Assignment[]): string {
|
|
242
|
+
if (assignments.length === 0) {
|
|
243
|
+
return `<div class="dt-empty">No active assignments</div>`;
|
|
244
|
+
}
|
|
245
|
+
return assignments
|
|
246
|
+
.map(
|
|
247
|
+
(a) => `
|
|
248
|
+
<div class="dt-card">
|
|
249
|
+
<div class="dt-card-title">${esc(a.experimentId)}</div>
|
|
250
|
+
<div class="dt-card-row"><span>Variation:</span> <span>${esc(a.variationId)}</span></div>
|
|
251
|
+
<div class="dt-card-row"><span>Index:</span> <span>${a.index}</span></div>
|
|
252
|
+
<div class="dt-card-row"><span>Control:</span> <span>${a.isControl ? "Yes" : "No"}</span></div>
|
|
253
|
+
<div class="dt-card-row"><span>Mutations:</span> <span>${a.mutations.length}</span></div>
|
|
254
|
+
<div class="dt-card-row"><span>CSS:</span> <span>${a.css ? "Yes" : "No"}</span></div>
|
|
255
|
+
<div class="dt-card-row"><span>JS:</span> <span>${a.js ? "Yes" : "No"}</span></div>
|
|
256
|
+
<div class="dt-card-row"><span>External CSS:</span> <span>${a.externalCssUrl ? "Yes" : "No"}</span></div>
|
|
257
|
+
<div class="dt-card-row"><span>External JS:</span> <span>${a.externalJsUrl ? "Yes" : "No"}</span></div>
|
|
258
|
+
<div class="dt-card-row"><span>Harness:</span> <span>${a.harnessSession ? `v${a.harnessSession.version ?? 0}` : "No"}</span></div>
|
|
259
|
+
</div>
|
|
260
|
+
`,
|
|
261
|
+
)
|
|
262
|
+
.join("");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private renderLog(): string {
|
|
266
|
+
if (eventLog.length === 0) {
|
|
267
|
+
return `<div class="dt-empty">No events logged yet</div>`;
|
|
268
|
+
}
|
|
269
|
+
const entries = [...eventLog].reverse().slice(0, 50);
|
|
270
|
+
return `<div class="dt-log">${entries
|
|
271
|
+
.map(
|
|
272
|
+
(e) => `<div class="dt-log-entry">
|
|
273
|
+
<span class="dt-log-time">${new Date(e.ts).toLocaleTimeString()}</span>
|
|
274
|
+
<span class="dt-log-type dt-type-${e.type}">${esc(e.type)}</span>
|
|
275
|
+
<span class="dt-log-label">${esc(e.label)}</span>
|
|
276
|
+
</div>`,
|
|
277
|
+
)
|
|
278
|
+
.join("")}</div>`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private renderForce(
|
|
282
|
+
experiments: Experiment[],
|
|
283
|
+
forceMap: Record<string, string> | null,
|
|
284
|
+
): string {
|
|
285
|
+
if (experiments.length === 0) {
|
|
286
|
+
return `<div class="dt-empty">No experiments available</div>`;
|
|
287
|
+
}
|
|
288
|
+
return experiments
|
|
289
|
+
.map(
|
|
290
|
+
(exp) => `
|
|
291
|
+
<div class="dt-card">
|
|
292
|
+
<div class="dt-card-title">${esc(exp.id)}</div>
|
|
293
|
+
<select class="dt-force-select" data-exp-id="${esc(exp.id)}">
|
|
294
|
+
<option value="">— Natural assignment —</option>
|
|
295
|
+
${exp.variations
|
|
296
|
+
.map(
|
|
297
|
+
(v, i) =>
|
|
298
|
+
`<option value="${esc(v.id)}" ${forceMap?.[exp.id] === v.id ? "selected" : ""}>${esc(v.id)}${v.isControl ? " (control)" : ""} — ${(v.weight * 100).toFixed(0)}%</option>`,
|
|
299
|
+
)
|
|
300
|
+
.join("")}
|
|
301
|
+
</select>
|
|
302
|
+
</div>
|
|
303
|
+
`,
|
|
304
|
+
)
|
|
305
|
+
.join("");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private attachDrag(): void {
|
|
309
|
+
const header = () => this.shadow.getElementById("dt-header");
|
|
310
|
+
const panel = () => this.shadow.getElementById("dt-panel");
|
|
311
|
+
|
|
312
|
+
const onMouseDown = (e: MouseEvent) => {
|
|
313
|
+
const p = panel();
|
|
314
|
+
if (!p) return;
|
|
315
|
+
const rect = p.getBoundingClientRect();
|
|
316
|
+
this.dragState = {
|
|
317
|
+
startX: e.clientX,
|
|
318
|
+
startY: e.clientY,
|
|
319
|
+
offsetX: rect.left,
|
|
320
|
+
offsetY: rect.top,
|
|
321
|
+
};
|
|
322
|
+
e.preventDefault();
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
326
|
+
if (!this.dragState) return;
|
|
327
|
+
const p = panel();
|
|
328
|
+
if (!p) return;
|
|
329
|
+
const dx = e.clientX - this.dragState.startX;
|
|
330
|
+
const dy = e.clientY - this.dragState.startY;
|
|
331
|
+
p.style.left = `${this.dragState.offsetX + dx}px`;
|
|
332
|
+
p.style.top = `${this.dragState.offsetY + dy}px`;
|
|
333
|
+
p.style.right = "auto";
|
|
334
|
+
p.style.bottom = "auto";
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const onMouseUp = () => {
|
|
338
|
+
this.dragState = null;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// Use capturing on the host document to ensure drag works even outside shadow
|
|
342
|
+
const attachHeader = () => {
|
|
343
|
+
const h = header();
|
|
344
|
+
if (h) h.addEventListener("mousedown", onMouseDown);
|
|
345
|
+
};
|
|
346
|
+
attachHeader();
|
|
347
|
+
|
|
348
|
+
// Re-attach after renders
|
|
349
|
+
const origRender = this.render.bind(this);
|
|
350
|
+
this.render = () => {
|
|
351
|
+
origRender();
|
|
352
|
+
attachHeader();
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
356
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// HTML escaping
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
function esc(s: string): string {
|
|
365
|
+
return s
|
|
366
|
+
.replace(/&/g, "&")
|
|
367
|
+
.replace(/</g, "<")
|
|
368
|
+
.replace(/>/g, ">")
|
|
369
|
+
.replace(/"/g, """);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
// Panel CSS
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
const PANEL_CSS = `
|
|
377
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
378
|
+
|
|
379
|
+
.dt-panel {
|
|
380
|
+
position: fixed;
|
|
381
|
+
bottom: 16px;
|
|
382
|
+
right: 16px;
|
|
383
|
+
width: 340px;
|
|
384
|
+
max-height: 480px;
|
|
385
|
+
background: #1a1a2e;
|
|
386
|
+
border: 1px solid #333;
|
|
387
|
+
border-radius: 8px;
|
|
388
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
389
|
+
font-size: 12px;
|
|
390
|
+
color: #e0e0e0;
|
|
391
|
+
z-index: 2147483647;
|
|
392
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.5);
|
|
393
|
+
display: flex;
|
|
394
|
+
flex-direction: column;
|
|
395
|
+
overflow: hidden;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.dt-header {
|
|
399
|
+
display: flex;
|
|
400
|
+
align-items: center;
|
|
401
|
+
padding: 8px 12px;
|
|
402
|
+
background: #16213e;
|
|
403
|
+
cursor: move;
|
|
404
|
+
user-select: none;
|
|
405
|
+
border-bottom: 1px solid #333;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.dt-title {
|
|
409
|
+
font-weight: 600;
|
|
410
|
+
font-size: 13px;
|
|
411
|
+
color: #fff;
|
|
412
|
+
flex: 1;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.dt-badge {
|
|
416
|
+
background: #0f3460;
|
|
417
|
+
color: #e0e0e0;
|
|
418
|
+
padding: 2px 8px;
|
|
419
|
+
border-radius: 10px;
|
|
420
|
+
font-size: 10px;
|
|
421
|
+
margin-right: 8px;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.dt-btn {
|
|
425
|
+
background: none;
|
|
426
|
+
border: none;
|
|
427
|
+
color: #aaa;
|
|
428
|
+
cursor: pointer;
|
|
429
|
+
font-size: 16px;
|
|
430
|
+
line-height: 1;
|
|
431
|
+
padding: 2px 6px;
|
|
432
|
+
}
|
|
433
|
+
.dt-btn:hover { color: #fff; }
|
|
434
|
+
|
|
435
|
+
.dt-tabs {
|
|
436
|
+
display: flex;
|
|
437
|
+
border-bottom: 1px solid #333;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.dt-tab {
|
|
441
|
+
flex: 1;
|
|
442
|
+
padding: 6px 8px;
|
|
443
|
+
background: none;
|
|
444
|
+
border: none;
|
|
445
|
+
color: #888;
|
|
446
|
+
cursor: pointer;
|
|
447
|
+
font-size: 11px;
|
|
448
|
+
text-align: center;
|
|
449
|
+
border-bottom: 2px solid transparent;
|
|
450
|
+
}
|
|
451
|
+
.dt-tab:hover { color: #ccc; }
|
|
452
|
+
.dt-tab.active { color: #53c1de; border-bottom-color: #53c1de; }
|
|
453
|
+
|
|
454
|
+
.dt-body {
|
|
455
|
+
overflow-y: auto;
|
|
456
|
+
max-height: 360px;
|
|
457
|
+
padding: 8px;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.dt-empty {
|
|
461
|
+
text-align: center;
|
|
462
|
+
color: #666;
|
|
463
|
+
padding: 24px;
|
|
464
|
+
}
|
|
465
|
+
.dt-empty.compact { padding: 12px; }
|
|
466
|
+
|
|
467
|
+
.dt-section-title {
|
|
468
|
+
color: #888;
|
|
469
|
+
font-size: 10px;
|
|
470
|
+
font-weight: 700;
|
|
471
|
+
letter-spacing: 0.04em;
|
|
472
|
+
margin: 4px 2px 6px;
|
|
473
|
+
text-transform: uppercase;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.dt-card {
|
|
477
|
+
background: #16213e;
|
|
478
|
+
border-radius: 6px;
|
|
479
|
+
padding: 8px 10px;
|
|
480
|
+
margin-bottom: 6px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.dt-card-title {
|
|
484
|
+
font-weight: 600;
|
|
485
|
+
color: #fff;
|
|
486
|
+
margin-bottom: 4px;
|
|
487
|
+
font-size: 12px;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.dt-card-row {
|
|
491
|
+
display: flex;
|
|
492
|
+
justify-content: space-between;
|
|
493
|
+
padding: 1px 0;
|
|
494
|
+
color: #aaa;
|
|
495
|
+
font-size: 11px;
|
|
496
|
+
}
|
|
497
|
+
.dt-card-row span:last-child { color: #e0e0e0; }
|
|
498
|
+
|
|
499
|
+
.dt-status {
|
|
500
|
+
border-radius: 3px;
|
|
501
|
+
font-size: 10px;
|
|
502
|
+
padding: 1px 5px;
|
|
503
|
+
text-transform: uppercase;
|
|
504
|
+
}
|
|
505
|
+
.dt-status-passed { background: #166534; color: #dcfce7 !important; }
|
|
506
|
+
.dt-status-failed { background: #7f1d1d; color: #fee2e2 !important; }
|
|
507
|
+
.dt-status-skipped { background: #334155; color: #e2e8f0 !important; }
|
|
508
|
+
|
|
509
|
+
.dt-gates {
|
|
510
|
+
margin-top: 6px;
|
|
511
|
+
border-top: 1px solid rgba(255,255,255,0.08);
|
|
512
|
+
padding-top: 5px;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.dt-gate {
|
|
516
|
+
display: grid;
|
|
517
|
+
grid-template-columns: 10px minmax(86px, 1fr) minmax(0, 1fr);
|
|
518
|
+
align-items: center;
|
|
519
|
+
gap: 6px;
|
|
520
|
+
padding: 3px 0;
|
|
521
|
+
color: #b8c1d1;
|
|
522
|
+
font-size: 11px;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.dt-dot {
|
|
526
|
+
width: 7px;
|
|
527
|
+
height: 7px;
|
|
528
|
+
border-radius: 999px;
|
|
529
|
+
display: inline-block;
|
|
530
|
+
}
|
|
531
|
+
.dt-dot-passed { background: #22c55e; }
|
|
532
|
+
.dt-dot-failed { background: #ef4444; }
|
|
533
|
+
.dt-dot-skipped { background: #64748b; }
|
|
534
|
+
.dt-gate-label { color: #e5e7eb; }
|
|
535
|
+
.dt-gate-reason {
|
|
536
|
+
color: #8b95a7;
|
|
537
|
+
overflow: hidden;
|
|
538
|
+
text-align: right;
|
|
539
|
+
text-overflow: ellipsis;
|
|
540
|
+
white-space: nowrap;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.dt-force-select {
|
|
544
|
+
width: 100%;
|
|
545
|
+
margin-top: 6px;
|
|
546
|
+
padding: 4px 8px;
|
|
547
|
+
background: #0f3460;
|
|
548
|
+
border: 1px solid #444;
|
|
549
|
+
color: #e0e0e0;
|
|
550
|
+
border-radius: 4px;
|
|
551
|
+
font-size: 11px;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.dt-log {
|
|
555
|
+
display: flex;
|
|
556
|
+
flex-direction: column;
|
|
557
|
+
gap: 2px;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.dt-log-entry {
|
|
561
|
+
display: flex;
|
|
562
|
+
gap: 6px;
|
|
563
|
+
padding: 3px 4px;
|
|
564
|
+
border-radius: 3px;
|
|
565
|
+
font-size: 11px;
|
|
566
|
+
line-height: 1.4;
|
|
567
|
+
}
|
|
568
|
+
.dt-log-entry:hover { background: rgba(255,255,255,0.03); }
|
|
569
|
+
|
|
570
|
+
.dt-log-time { color: #666; flex-shrink: 0; font-family: monospace; }
|
|
571
|
+
.dt-log-type { flex-shrink: 0; font-weight: 600; text-transform: uppercase; font-size: 9px; padding: 1px 4px; border-radius: 3px; }
|
|
572
|
+
.dt-log-label { color: #ccc; }
|
|
573
|
+
|
|
574
|
+
.dt-type-init { background: #1a6; color: #fff; }
|
|
575
|
+
.dt-type-eval { background: #36a; color: #fff; }
|
|
576
|
+
.dt-type-apply { background: #a63; color: #fff; }
|
|
577
|
+
.dt-type-track { background: #63a; color: #fff; }
|
|
578
|
+
.dt-type-goal { background: #a36; color: #fff; }
|
|
579
|
+
.dt-type-qa { background: #3aa; color: #fff; }
|
|
580
|
+
.dt-type-guard { background: #666; color: #fff; }
|
|
581
|
+
.dt-type-tracker { background: #636; color: #fff; }
|
|
582
|
+
`;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
class FakeElement {
|
|
4
|
+
constructor(
|
|
5
|
+
private readonly id: string,
|
|
6
|
+
private readonly inQaAssistantHost = false,
|
|
7
|
+
) {}
|
|
8
|
+
|
|
9
|
+
getAttribute(name: string): string | null {
|
|
10
|
+
return name === "id" ? this.id : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
closest(selector: string): FakeElement | null {
|
|
14
|
+
return this.inQaAssistantHost && selector.includes("#drip-qa-assistant-host") ? this : null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("observeDomChanges", () => {
|
|
19
|
+
const originalDocument = (globalThis as any).document;
|
|
20
|
+
const originalElement = (globalThis as any).Element;
|
|
21
|
+
const originalMutationObserver = (globalThis as any).MutationObserver;
|
|
22
|
+
const originalWindow = (globalThis as any).window;
|
|
23
|
+
let mutationCallback: MutationCallback | null = null;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.useFakeTimers();
|
|
27
|
+
(globalThis as any).Element = FakeElement;
|
|
28
|
+
(globalThis as any).document = {
|
|
29
|
+
documentElement: new FakeElement("html"),
|
|
30
|
+
};
|
|
31
|
+
(globalThis as any).window = {
|
|
32
|
+
clearTimeout,
|
|
33
|
+
setTimeout,
|
|
34
|
+
};
|
|
35
|
+
(globalThis as any).MutationObserver = class MutationObserver {
|
|
36
|
+
constructor(callback: MutationCallback) {
|
|
37
|
+
mutationCallback = callback;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
observe = vi.fn();
|
|
41
|
+
disconnect = vi.fn();
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
vi.useRealTimers();
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
(globalThis as any).document = originalDocument;
|
|
49
|
+
(globalThis as any).Element = originalElement;
|
|
50
|
+
(globalThis as any).MutationObserver = originalMutationObserver;
|
|
51
|
+
(globalThis as any).window = originalWindow;
|
|
52
|
+
mutationCallback = null;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("ignores mutations inside the QA Assistant host", async () => {
|
|
56
|
+
const observer = vi.fn();
|
|
57
|
+
const { observeDomChanges } = await import("./dom-trigger");
|
|
58
|
+
observeDomChanges(observer, 25);
|
|
59
|
+
|
|
60
|
+
mutationCallback?.([
|
|
61
|
+
{
|
|
62
|
+
addedNodes: [new FakeElement("assistant-button", true)],
|
|
63
|
+
type: "childList",
|
|
64
|
+
} as unknown as MutationRecord,
|
|
65
|
+
], {} as MutationObserver);
|
|
66
|
+
vi.advanceTimersByTime(25);
|
|
67
|
+
|
|
68
|
+
expect(observer).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { registerMutedObserver, unregisterMutedObserver } from "./observer-mute";
|
|
2
|
+
|
|
3
|
+
type DomChangeObserver = () => void;
|
|
4
|
+
|
|
5
|
+
const QA_ASSISTANT_HOST_SELECTOR = "#" + ["drip", "qa", "assistant", "host"].join("-");
|
|
6
|
+
const APEX_HOST_SELECTOR = ["#drip-qa-toolbar", QA_ASSISTANT_HOST_SELECTOR, "#drip-devtools-host"].join(",");
|
|
7
|
+
|
|
8
|
+
function isApexNode(node: Node): boolean {
|
|
9
|
+
if (!(node instanceof Element)) return false;
|
|
10
|
+
const id = node.getAttribute("id") || "";
|
|
11
|
+
return id.startsWith("drip-") || node.closest?.(APEX_HOST_SELECTOR) != null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function observeDomChanges(
|
|
15
|
+
observer: DomChangeObserver,
|
|
16
|
+
debounceMs = 250,
|
|
17
|
+
): () => void {
|
|
18
|
+
if (typeof MutationObserver === "undefined" || !document.documentElement) {
|
|
19
|
+
return () => {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let timer: number | null = null;
|
|
23
|
+
const schedule = () => {
|
|
24
|
+
if (timer !== null) window.clearTimeout(timer);
|
|
25
|
+
timer = window.setTimeout(() => {
|
|
26
|
+
timer = null;
|
|
27
|
+
observer();
|
|
28
|
+
}, debounceMs);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const onRecords: MutationCallback = (mutations) => {
|
|
32
|
+
const relevant = mutations.some((mutation) => {
|
|
33
|
+
if (mutation.type === "attributes") {
|
|
34
|
+
return mutation.target instanceof Element && !isApexNode(mutation.target);
|
|
35
|
+
}
|
|
36
|
+
return Array.from(mutation.addedNodes).some((node) => !isApexNode(node));
|
|
37
|
+
});
|
|
38
|
+
if (relevant) schedule();
|
|
39
|
+
};
|
|
40
|
+
const mutationObserver = new MutationObserver(onRecords);
|
|
41
|
+
|
|
42
|
+
registerMutedObserver(mutationObserver, onRecords);
|
|
43
|
+
mutationObserver.observe(document.documentElement, {
|
|
44
|
+
childList: true,
|
|
45
|
+
subtree: true,
|
|
46
|
+
attributes: true,
|
|
47
|
+
attributeFilter: ["class", "style", "hidden", "aria-hidden"],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return () => {
|
|
51
|
+
if (timer !== null) window.clearTimeout(timer);
|
|
52
|
+
mutationObserver.disconnect();
|
|
53
|
+
unregisterMutedObserver(mutationObserver);
|
|
54
|
+
};
|
|
55
|
+
}
|