@agent-native/core 0.216.0 → 0.218.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/app-config/analytics.js +1 -1
- package/dist/client/agent-chat/index.d.ts +1 -1
- package/dist/client/analytics-session.d.ts +7 -0
- package/dist/client/analytics-session.js +27 -1
- package/dist/client/analytics.js +110 -15
- package/dist/client/consent.d.ts +12 -0
- package/dist/client/consent.js +132 -2
- package/dist/client/onboarding/index.d.ts +1 -0
- package/dist/client/onboarding/index.js +1 -0
- package/dist/client/onboarding/use-onboarding-checklist.d.ts +23 -0
- package/dist/client/onboarding/use-onboarding-checklist.js +115 -0
- package/dist/client/session-replay.d.ts +1 -1
- package/dist/client/session-replay.js +142 -3
- package/dist/client/use-chat-thread-list.d.ts +3 -0
- package/dist/client/use-chat-thread-list.js +23 -7
- package/dist/collab/routes.d.ts +1 -1
- package/dist/localization/consent-banner-messages.d.ts +16 -0
- package/dist/localization/consent-banner-messages.js +149 -0
- package/dist/localization/core-messages.js +4 -0
- package/dist/localization/default-messages.d.ts +1 -0
- package/dist/localization/default-messages.js +2 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +7 -7
- package/dist/onboarding/index.d.ts +2 -1
- package/dist/onboarding/index.js +1 -0
- package/dist/onboarding/plugin.js +55 -46
- package/dist/onboarding/status.d.ts +15 -0
- package/dist/onboarding/status.js +59 -0
- package/dist/onboarding/types.d.ts +12 -0
- package/dist/onboarding/types.js +2 -1
- package/dist/org/workspace-app-access.js +19 -9
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +10 -10
- package/dist/server/agent-capabilities.js +13 -6
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/analytics.d.ts +0 -1
- package/dist/server/analytics.js +81 -20
- package/dist/server/builder-api-auth.d.ts +7 -0
- package/dist/server/builder-api-auth.js +24 -0
- package/dist/server/builder-oauth.d.ts +5 -1
- package/dist/server/builder-oauth.js +5 -0
- package/dist/server/first-touch-route.d.ts +2 -2
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/shared/consent.d.ts +2 -0
- package/dist/shared/consent.js +4 -0
- package/dist/templates/builder-code-starter/template-layer.json +1 -0
- package/dist/templates/chat/app/components/layout/Sidebar.tsx +62 -101
- package/dist/triggers/actions/manage-automation.d.ts +1 -1
- package/dist/usage/actions/get-usage-metrics.js +3 -3
- package/docs/content/components.mdx +19 -18
- package/docs/content/deployment-environment-variables.mdx +1 -1
- package/docs/content/environment-variables.mdx +1 -1
- package/docs/content/sharing.mdx +4 -0
- package/docs/content/toolkit-sharing.mdx +31 -51
- package/migration-manifest.json +1 -1
- package/package.json +5 -3
- package/src/templates/builder-code-starter/template-layer.json +1 -0
- package/src/templates/chat/app/components/layout/Sidebar.tsx +62 -101
- package/dist/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
- package/src/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
|
@@ -32,6 +32,6 @@ export const analyticsConfig = z.object({
|
|
|
32
32
|
.optional()
|
|
33
33
|
.meta({
|
|
34
34
|
env: "AGENT_NATIVE_HEAD_SCRIPT_SRC",
|
|
35
|
-
doc: "HTTPS URL of a script
|
|
35
|
+
doc: "HTTPS URL of a script added to every server-rendered page, for deployment-specific tooling such as support or feedback widgets. Where the consent banner shows, it waits for analytics consent, like Google Analytics.",
|
|
36
36
|
}),
|
|
37
37
|
});
|
|
@@ -19,7 +19,7 @@ export { AGENT_NATIVE_MCP_APP_HOST_MESSAGE_TYPES, getMcpAppHostContext, initiali
|
|
|
19
19
|
export { buildDynamicAgentSuggestions, dedupeSuggestions, mergeAgentSuggestions, normalizeAgentDynamicSuggestionsConfig, useAgentDynamicSuggestions, useAgentDynamicSuggestionsResult, type AgentDynamicSuggestionContext, type AgentDynamicSuggestionsConfig, type AgentDynamicSuggestionsOption, } from "../dynamic-suggestions.js";
|
|
20
20
|
export { clearChatStorage } from "../chat/storage.js";
|
|
21
21
|
export { isAssistantChatHistoryVersion, coerceAssistantChatHistoryDate, type AssistantChatHistoryDate, type AssistantChatHistoryVersion, } from "../chat/assistant-chat-history-version.js";
|
|
22
|
-
export { ChatThreadListConfigError, useChatThreadList, type UseChatThreadListOptions, type UseChatThreadListResult, } from "../use-chat-thread-list.js";
|
|
22
|
+
export { ChatThreadListConfigError, useChatThreadList, type UseChatThreadListOptions, type UseChatThreadListResult, type ChatThreadListRefreshOutcome, } from "../use-chat-thread-list.js";
|
|
23
23
|
export { ChatThreadListError, fetchChatThreadListPage, notifyChatThreadsUpdated, useChatThreads, type ChatThreadAccessBasis, type ChatThreadListPage, type ChatThreadListRow, type ChatThreadListPageRequest, type ChatThreadOwnership, type ChatThreadVisibility, type ChatThreadScope, type ChatThreadSnapshot, type ChatThreadSummary, type ChatThreadData, type ChatThreadShareLink, type ChatThreadShareState, type UseChatThreadsOptions, } from "../use-chat-threads.js";
|
|
24
24
|
export { type AgentCallProgress, type ContentPart, } from "../sse-event-processor.js";
|
|
25
25
|
export { useRunStuckDetection, useAbortRun, type RunStuckState, type UseRunStuckDetectionOptions, } from "../use-run-stuck-detection.js";
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/** One id for this page load, shared by every pageview until it reloads. */
|
|
2
2
|
export declare function getAnalyticsPageLoadId(): string;
|
|
3
|
+
/**
|
|
4
|
+
* The visitor id, `an_aid`, waits for analytics consent where the banner
|
|
5
|
+
* applies, and for the server's region rather than the time zone's guess,
|
|
6
|
+
* since a sent id can't be recalled. Until then this returns `undefined` and
|
|
7
|
+
* stores nothing, so events still count but can't be tied to one visitor
|
|
8
|
+
* across pages.
|
|
9
|
+
*/
|
|
3
10
|
export declare function getOrCreateAnalyticsAnonymousId(): string | undefined;
|
|
4
11
|
export declare function setAnalyticsSessionId(sessionId: string): string | undefined;
|
|
5
12
|
export declare function clearAnalyticsSessionId(): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { serializeAnalyticsAnonymousIdCookie } from "../shared/analytics-anonymous-id.js";
|
|
1
|
+
import { ANALYTICS_ANONYMOUS_ID_COOKIE_NAME, serializeAnalyticsAnonymousIdCookie, } from "../shared/analytics-anonymous-id.js";
|
|
2
2
|
import { ANALYTICS_SESSION_ID_COOKIE_NAME, ANALYTICS_SESSION_ID_MAX_LENGTH, normalizeAnalyticsSessionId, serializeAnalyticsSessionIdCookie, } from "../shared/analytics-session-id.js";
|
|
3
|
+
import { consentDecision, whenConsentAllows } from "./consent.js";
|
|
3
4
|
const ANONYMOUS_ID_STORAGE_KEY = "agent-native.anonymous_id";
|
|
4
5
|
const SESSION_ID_STORAGE_KEY = "agent-native.session_id";
|
|
5
6
|
const SESSION_ID_PIN_STORAGE_KEY = "agent-native.session_id_pin";
|
|
@@ -72,9 +73,34 @@ export function getAnalyticsPageLoadId() {
|
|
|
72
73
|
pageLoadId ??= generateVisitorId();
|
|
73
74
|
return pageLoadId;
|
|
74
75
|
}
|
|
76
|
+
let anonymousIdConsentWatched = false;
|
|
77
|
+
function clearAnalyticsAnonymousId() {
|
|
78
|
+
safeStorageRemove(ANONYMOUS_ID_STORAGE_KEY);
|
|
79
|
+
try {
|
|
80
|
+
document.cookie = `${ANALYTICS_ANONYMOUS_ID_COOKIE_NAME}=; path=/; max-age=0; SameSite=Lax`;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// coercion-ok: a frame that can't read cookies never stored one.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The visitor id, `an_aid`, waits for analytics consent where the banner
|
|
88
|
+
* applies, and for the server's region rather than the time zone's guess,
|
|
89
|
+
* since a sent id can't be recalled. Until then this returns `undefined` and
|
|
90
|
+
* stores nothing, so events still count but can't be tied to one visitor
|
|
91
|
+
* across pages.
|
|
92
|
+
*/
|
|
75
93
|
export function getOrCreateAnalyticsAnonymousId() {
|
|
76
94
|
if (typeof window === "undefined")
|
|
77
95
|
return undefined;
|
|
96
|
+
if (!anonymousIdConsentWatched) {
|
|
97
|
+
anonymousIdConsentWatched = true;
|
|
98
|
+
whenConsentAllows("performance", () => { }, {
|
|
99
|
+
revoke: clearAnalyticsAnonymousId,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (consentDecision("performance") !== true)
|
|
103
|
+
return undefined;
|
|
78
104
|
let id = safeStorageGet(ANONYMOUS_ID_STORAGE_KEY);
|
|
79
105
|
if (!id) {
|
|
80
106
|
id = generateVisitorId();
|
package/dist/client/analytics.js
CHANGED
|
@@ -16,7 +16,7 @@ import { agentNativePath } from "./api-path.js";
|
|
|
16
16
|
import { agentNativeApiDisabledReason } from "./api-surface.js";
|
|
17
17
|
import { injectedAgentNativeConfig } from "./app-config.js";
|
|
18
18
|
import { clientBuildId } from "./build-compatibility.js";
|
|
19
|
-
import { consentAllowsNow, whenConsentAllows } from "./consent.js";
|
|
19
|
+
import { consentAllowsNow, consentDecision, getConsentChoice, whenConsentAllows, whenConsentDecided, } from "./consent.js";
|
|
20
20
|
import { clientFailureContext } from "./failure-report.js";
|
|
21
21
|
import { replayEndpointFromAnalyticsEndpoint } from "./session-replay-endpoint.js";
|
|
22
22
|
import { scheduleAfterPaint } from "./use-after-paint.js";
|
|
@@ -39,6 +39,7 @@ let _amplitudeModule = null;
|
|
|
39
39
|
let _amplitudeLoadPromise = null;
|
|
40
40
|
let _amplitudeApiKey = null;
|
|
41
41
|
let _pendingAmplitudeEvents = [];
|
|
42
|
+
let _amplitudeConsent = "unasked";
|
|
42
43
|
let _sentryInitialized = false;
|
|
43
44
|
let _sentryModule = null;
|
|
44
45
|
let _sentryLoadPromise = null;
|
|
@@ -881,20 +882,82 @@ function isSyntheticBrowserTraffic() {
|
|
|
881
882
|
return (typeof window !== "undefined" &&
|
|
882
883
|
isSyntheticTrafficValue(window.__AGENT_NATIVE_SYNTHETIC_TRAFFIC__));
|
|
883
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* Amplitude keeps its device id in a cookie on the widest domain it can set,
|
|
887
|
+
* and its marketing touch and unsent events beside it, all named from the
|
|
888
|
+
* API key. A reject clears them at every level of this host.
|
|
889
|
+
*/
|
|
890
|
+
function clearAmplitudeStorage(apiKey) {
|
|
891
|
+
const key = apiKey.slice(0, 10);
|
|
892
|
+
const names = [
|
|
893
|
+
`AMP_${key}`,
|
|
894
|
+
`AMP_MKTG_${key}`,
|
|
895
|
+
`AMP_MKTG_ORIGINAL_${key}`,
|
|
896
|
+
`AMP_unsent_${key}`,
|
|
897
|
+
`amp_${apiKey.slice(0, 6)}`,
|
|
898
|
+
];
|
|
899
|
+
try {
|
|
900
|
+
const labels = window.location.hostname.split(".");
|
|
901
|
+
const domains = [""];
|
|
902
|
+
for (let i = 0; i < labels.length - 1; i += 1) {
|
|
903
|
+
domains.push(`; domain=.${labels.slice(i).join(".")}`);
|
|
904
|
+
}
|
|
905
|
+
for (const name of names) {
|
|
906
|
+
for (const domain of domains) {
|
|
907
|
+
document.cookie = `${name}=; path=/; max-age=0${domain}`;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
// coercion-ok: a sandboxed iframe can't read or write cookies, so
|
|
913
|
+
// Amplitude stored none here.
|
|
914
|
+
}
|
|
915
|
+
for (const name of names)
|
|
916
|
+
safeStorageRemove(name);
|
|
917
|
+
}
|
|
884
918
|
function ensureAmplitude() {
|
|
885
919
|
if (isSyntheticBrowserTraffic())
|
|
886
920
|
return false;
|
|
887
|
-
if (_amplitudeInitialized)
|
|
888
|
-
return true;
|
|
889
921
|
const key = import.meta.env
|
|
890
922
|
?.VITE_AMPLITUDE_API_KEY;
|
|
891
923
|
if (!key)
|
|
892
924
|
return false;
|
|
893
925
|
_amplitudeApiKey = key;
|
|
926
|
+
if (_amplitudeConsent !== "granted") {
|
|
927
|
+
if (_amplitudeConsent === "unasked") {
|
|
928
|
+
_amplitudeConsent = "waiting";
|
|
929
|
+
// Amplitude keeps a device id in the browser, so where the banner
|
|
930
|
+
// applies it waits for performance cookies.
|
|
931
|
+
whenConsentAllows("performance", () => {
|
|
932
|
+
_amplitudeConsent = "granted";
|
|
933
|
+
// Held before the region answered: with a choice on record, the
|
|
934
|
+
// visitor had to choose, so what came before it never goes out.
|
|
935
|
+
if (getConsentChoice())
|
|
936
|
+
_pendingAmplitudeEvents = [];
|
|
937
|
+
_amplitudeModule?.setOptOut(false);
|
|
938
|
+
ensureAmplitude();
|
|
939
|
+
}, {
|
|
940
|
+
revoke: () => {
|
|
941
|
+
_amplitudeConsent = "revoked";
|
|
942
|
+
_pendingAmplitudeEvents = [];
|
|
943
|
+
_amplitudeModule?.setOptOut(true);
|
|
944
|
+
// A later accept starts a new device id, not the one in memory.
|
|
945
|
+
_amplitudeModule?.reset();
|
|
946
|
+
clearAmplitudeStorage(key);
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
if (_amplitudeInitialized)
|
|
953
|
+
return true;
|
|
894
954
|
if (_amplitudeLoadPromise)
|
|
895
955
|
return false;
|
|
896
956
|
_amplitudeLoadPromise = loadOptionalPeer("@amplitude/analytics-browser", () => import("@amplitude/analytics-browser"))
|
|
897
957
|
.then((module) => {
|
|
958
|
+
// A reject while the SDK loaded: the next accept initializes it.
|
|
959
|
+
if (_amplitudeConsent !== "granted")
|
|
960
|
+
return null;
|
|
898
961
|
module.init(key, { autocapture: false });
|
|
899
962
|
_amplitudeModule = module;
|
|
900
963
|
_amplitudeInitialized = true;
|
|
@@ -2155,15 +2218,30 @@ function sendAgentNativeAnalytics(name, properties) {
|
|
|
2155
2218
|
?.VITE_AGENT_NATIVE_ANALYTICS_ENDPOINT ||
|
|
2156
2219
|
AGENT_NATIVE_ANALYTICS_DEFAULT_ENDPOINT, window.location.hostname);
|
|
2157
2220
|
const userId = typeof properties.userId === "string" ? properties.userId : undefined;
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2221
|
+
const sessionId = getOrCreateSessionId();
|
|
2222
|
+
const timestamp = new Date().toISOString();
|
|
2223
|
+
whenConsentDecided((decided) => {
|
|
2224
|
+
const anonymousId = decided ? getOrCreateAnonymousId() : undefined;
|
|
2225
|
+
postAgentNativeAnalytics(endpoint, JSON.stringify({
|
|
2226
|
+
publicKey,
|
|
2227
|
+
event: name,
|
|
2228
|
+
// The visitor id waits for analytics consent; mark the event so
|
|
2229
|
+
// reports can tell it from one that lost its id some other way, and
|
|
2230
|
+
// a refusal from a page that closed before the region was known.
|
|
2231
|
+
properties: anonymousId
|
|
2232
|
+
? properties
|
|
2233
|
+
: {
|
|
2234
|
+
...properties,
|
|
2235
|
+
analytics_consent: decided ? "withheld" : "unknown",
|
|
2236
|
+
},
|
|
2237
|
+
userId,
|
|
2238
|
+
anonymousId,
|
|
2239
|
+
sessionId,
|
|
2240
|
+
timestamp,
|
|
2241
|
+
}));
|
|
2166
2242
|
});
|
|
2243
|
+
}
|
|
2244
|
+
function postAgentNativeAnalytics(endpoint, body) {
|
|
2167
2245
|
try {
|
|
2168
2246
|
if (navigator.sendBeacon) {
|
|
2169
2247
|
const sent = navigator.sendBeacon(endpoint, body);
|
|
@@ -2187,17 +2265,34 @@ function createTrackingAliasId() {
|
|
|
2187
2265
|
? crypto.randomUUID()
|
|
2188
2266
|
: undefined;
|
|
2189
2267
|
}
|
|
2268
|
+
function pageHasGoogleTags() {
|
|
2269
|
+
const holder = window;
|
|
2270
|
+
return Boolean(holder.__AN_CONSENT_LAYERS__ ||
|
|
2271
|
+
window.__AGENT_NATIVE_GA_GTAG__ ||
|
|
2272
|
+
window.gtag);
|
|
2273
|
+
}
|
|
2190
2274
|
function emitBrowserTrackingEvent(name, props, options = {}) {
|
|
2191
2275
|
const { agentNativeProperties = props, gtagProperties = props, sendGtag = true, } = options;
|
|
2192
2276
|
const amplitudeProps = amplitudeEventProperties(name, props);
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2277
|
+
// Without Google tags there is nothing to hold, and asking for the region
|
|
2278
|
+
// would be a request a scoped embed (an MCP App widget) may not make.
|
|
2279
|
+
if (sendGtag && pageHasGoogleTags()) {
|
|
2280
|
+
whenConsentDecided(() => {
|
|
2281
|
+
if (consentDecision("performance") !== true)
|
|
2282
|
+
return;
|
|
2283
|
+
const gtag = window.__AGENT_NATIVE_GA_GTAG__ ?? window.gtag;
|
|
2284
|
+
gtag?.("event", name.replace(/\s+/g, "_"), gtagProperties);
|
|
2285
|
+
});
|
|
2196
2286
|
}
|
|
2197
2287
|
if (ensureAmplitude()) {
|
|
2198
2288
|
_amplitudeModule?.track(name, amplitudeProps);
|
|
2199
2289
|
}
|
|
2200
|
-
else if (_amplitudeApiKey
|
|
2290
|
+
else if (_amplitudeApiKey &&
|
|
2291
|
+
// Only while it loads after an accept, or before the region is known;
|
|
2292
|
+
// the region's answer clears what's held if the visitor must choose.
|
|
2293
|
+
(_amplitudeConsent === "granted" ||
|
|
2294
|
+
(_amplitudeConsent !== "revoked" &&
|
|
2295
|
+
consentDecision("performance") === null))) {
|
|
2201
2296
|
if (_pendingAmplitudeEvents.length < 100) {
|
|
2202
2297
|
_pendingAmplitudeEvents.push([name, amplitudeProps]);
|
|
2203
2298
|
}
|
package/dist/client/consent.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export declare function resolveConsentRegion(): Promise<ConsentRegion>;
|
|
|
13
13
|
* whatever this let through.
|
|
14
14
|
*/
|
|
15
15
|
export declare function consentAllowsNow(category: ConsentCategory): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Whether `category` is allowed, from the visitor's choice or the server's
|
|
18
|
+
* region; `null` until one of them is known, where `consentAllowsNow` would
|
|
19
|
+
* guess from the time zone. For what `revoke` can't undo once it's sent.
|
|
20
|
+
*/
|
|
21
|
+
export declare function consentDecision(category: ConsentCategory): boolean | null;
|
|
16
22
|
export interface WhenConsentAllowsOptions {
|
|
17
23
|
/**
|
|
18
24
|
* Run now on the time zone's guess instead of waiting for the server, and
|
|
@@ -27,6 +33,12 @@ export interface WhenConsentAllowsOptions {
|
|
|
27
33
|
* and inside it when the visitor accepts. A later "reject" calls `revoke`.
|
|
28
34
|
*/
|
|
29
35
|
export declare function whenConsentAllows(category: ConsentCategory, run: () => void, options?: WhenConsentAllowsOptions): void;
|
|
36
|
+
/**
|
|
37
|
+
* Run `send` once the visitor's choice or the server's region is known, so
|
|
38
|
+
* the time zone's guess never sends what consent covers. `decided` is false
|
|
39
|
+
* when the page closes first; send then without what consent covers.
|
|
40
|
+
*/
|
|
41
|
+
export declare function whenConsentDecided(send: (decided: boolean) => void): void;
|
|
30
42
|
/** Store the visitor's choice for every app on this site, and apply it. */
|
|
31
43
|
export declare function setConsentChoice(choice: ConsentChoice): void;
|
|
32
44
|
/** Reopen the banner so the visitor can change their choice. */
|
package/dist/client/consent.js
CHANGED
|
@@ -8,6 +8,8 @@ const REGION_STORAGE_KEY = "an_consent_region";
|
|
|
8
8
|
* injects into the page, queue `[category, run]` here.
|
|
9
9
|
*/
|
|
10
10
|
const SCRIPT_QUEUE_KEY = "__AN_CONSENT_QUEUE__";
|
|
11
|
+
/** The Google data layers the server's Consent Mode default script set up. */
|
|
12
|
+
const GOOGLE_CONSENT_LAYERS_KEY = "__AN_CONSENT_LAYERS__";
|
|
11
13
|
// One state per page, however many copies of this module a bundle carries.
|
|
12
14
|
const STATE_KEY = Symbol.for("agent-native.client.consent");
|
|
13
15
|
function state() {
|
|
@@ -18,6 +20,7 @@ function state() {
|
|
|
18
20
|
waiters: [],
|
|
19
21
|
listeners: new Set(),
|
|
20
22
|
settingsOpen: false,
|
|
23
|
+
pageChoice: null,
|
|
21
24
|
});
|
|
22
25
|
}
|
|
23
26
|
function readCookie(name) {
|
|
@@ -39,7 +42,7 @@ function readCookie(name) {
|
|
|
39
42
|
}
|
|
40
43
|
/** The visitor's stored choice, or `null` before they've made one. */
|
|
41
44
|
export function getConsentChoice() {
|
|
42
|
-
return parseConsent(readCookie(CONSENT_COOKIE_NAME));
|
|
45
|
+
return (parseConsent(readCookie(CONSENT_COOKIE_NAME)) ?? state().pageChoice ?? null);
|
|
43
46
|
}
|
|
44
47
|
function cookieDomain() {
|
|
45
48
|
const host = typeof window === "undefined" ? "" : window.location.hostname;
|
|
@@ -183,21 +186,69 @@ export function consentAllowsNow(category) {
|
|
|
183
186
|
void resolveConsentRegion();
|
|
184
187
|
return allows(category, regionGuess());
|
|
185
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Whether `category` is allowed, from the visitor's choice or the server's
|
|
191
|
+
* region; `null` until one of them is known, where `consentAllowsNow` would
|
|
192
|
+
* guess from the time zone. For what `revoke` can't undo once it's sent.
|
|
193
|
+
*/
|
|
194
|
+
export function consentDecision(category) {
|
|
195
|
+
if (typeof window === "undefined")
|
|
196
|
+
return false;
|
|
197
|
+
const choice = getConsentChoice();
|
|
198
|
+
if (choice)
|
|
199
|
+
return choice[category];
|
|
200
|
+
const region = state().region ?? readCachedRegion();
|
|
201
|
+
if (region)
|
|
202
|
+
return region === "none";
|
|
203
|
+
void resolveConsentRegion();
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Google's tags can't be unloaded, but they follow Consent Mode: a reject
|
|
208
|
+
* stops what's already on the page from storing or sending identifiers.
|
|
209
|
+
*/
|
|
210
|
+
function updateGoogleConsent(region) {
|
|
211
|
+
const holder = window;
|
|
212
|
+
const layers = holder[GOOGLE_CONSENT_LAYERS_KEY];
|
|
213
|
+
if (!Array.isArray(layers))
|
|
214
|
+
return;
|
|
215
|
+
const grant = (category) => allows(category, region) ? "granted" : "denied";
|
|
216
|
+
const update = {
|
|
217
|
+
analytics_storage: grant("performance"),
|
|
218
|
+
ad_storage: grant("targeting"),
|
|
219
|
+
ad_user_data: grant("targeting"),
|
|
220
|
+
ad_personalization: grant("targeting"),
|
|
221
|
+
};
|
|
222
|
+
for (const name of layers) {
|
|
223
|
+
if (typeof name !== "string")
|
|
224
|
+
continue;
|
|
225
|
+
const layer = (holder[name] ??= []);
|
|
226
|
+
// gtag reads an `arguments` object from the data layer, never an array.
|
|
227
|
+
(function gtag(..._args) {
|
|
228
|
+
layer.push(arguments);
|
|
229
|
+
})("consent", "update", update);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
186
232
|
function settle() {
|
|
187
233
|
const region = state().region;
|
|
234
|
+
if (region || getConsentChoice())
|
|
235
|
+
updateGoogleConsent(region);
|
|
188
236
|
for (const waiter of state().waiters) {
|
|
189
237
|
const allowed = allows(waiter.category, region);
|
|
190
238
|
if (allowed && !waiter.ran) {
|
|
191
239
|
waiter.ran = true;
|
|
192
240
|
isolate(waiter.run);
|
|
193
241
|
}
|
|
194
|
-
else if (!allowed && region) {
|
|
242
|
+
else if (!allowed && (region || getConsentChoice())) {
|
|
195
243
|
// Also clears what an earlier page stored before the banner existed.
|
|
196
244
|
waiter.ran = false;
|
|
197
245
|
if (waiter.revoke)
|
|
198
246
|
isolate(waiter.revoke);
|
|
199
247
|
}
|
|
200
248
|
}
|
|
249
|
+
// A choice stored here or in another tab decides before the region does.
|
|
250
|
+
if (region || getConsentChoice())
|
|
251
|
+
releaseHeldForDecision?.(true);
|
|
201
252
|
}
|
|
202
253
|
/**
|
|
203
254
|
* One callback throwing must not skip the waiters after it, or a later revoke
|
|
@@ -234,15 +285,93 @@ export function whenConsentAllows(category, run, options = {}) {
|
|
|
234
285
|
waiter.ran = true;
|
|
235
286
|
isolate(run);
|
|
236
287
|
}
|
|
288
|
+
else if (state().region || getConsentChoice()) {
|
|
289
|
+
// A reject decided before this caller asked still clears what it stored.
|
|
290
|
+
if (options.revoke)
|
|
291
|
+
isolate(options.revoke);
|
|
292
|
+
}
|
|
237
293
|
if (!state().region)
|
|
238
294
|
void resolveConsentRegion();
|
|
239
295
|
}
|
|
296
|
+
let heldForDecision = null;
|
|
297
|
+
let releaseHeldForDecision = null;
|
|
298
|
+
/**
|
|
299
|
+
* Run `send` once the visitor's choice or the server's region is known, so
|
|
300
|
+
* the time zone's guess never sends what consent covers. `decided` is false
|
|
301
|
+
* when the page closes first; send then without what consent covers.
|
|
302
|
+
*/
|
|
303
|
+
export function whenConsentDecided(send) {
|
|
304
|
+
if (consentDecision("performance") !== null) {
|
|
305
|
+
send(true);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (!heldForDecision) {
|
|
309
|
+
const held = [];
|
|
310
|
+
heldForDecision = held;
|
|
311
|
+
const release = (decided) => {
|
|
312
|
+
if (heldForDecision !== held)
|
|
313
|
+
return;
|
|
314
|
+
heldForDecision = null;
|
|
315
|
+
releaseHeldForDecision = null;
|
|
316
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
317
|
+
for (const run of held)
|
|
318
|
+
run(decided);
|
|
319
|
+
};
|
|
320
|
+
const onPageHide = () => release(false);
|
|
321
|
+
releaseHeldForDecision = release;
|
|
322
|
+
window.addEventListener("pagehide", onPageHide);
|
|
323
|
+
void resolveConsentRegion().then(() => release(true));
|
|
324
|
+
}
|
|
325
|
+
heldForDecision.push(send);
|
|
326
|
+
}
|
|
327
|
+
const CONSENT_CHANNEL_NAME = "agent-native-consent";
|
|
328
|
+
let seenChoice = null;
|
|
329
|
+
let consentChannel = null;
|
|
330
|
+
/**
|
|
331
|
+
* Apply a choice another tab or app stored: at once in other tabs of this
|
|
332
|
+
* app, and on the next focus for another app on the site.
|
|
333
|
+
*/
|
|
334
|
+
function recheckStoredChoice() {
|
|
335
|
+
const raw = readCookie(CONSENT_COOKIE_NAME);
|
|
336
|
+
if (raw === seenChoice)
|
|
337
|
+
return;
|
|
338
|
+
seenChoice = raw;
|
|
339
|
+
state().settingsOpen = false;
|
|
340
|
+
settle();
|
|
341
|
+
notify();
|
|
342
|
+
}
|
|
343
|
+
function watchOtherTabs() {
|
|
344
|
+
if (typeof window === "undefined" || typeof document === "undefined")
|
|
345
|
+
return;
|
|
346
|
+
seenChoice = readCookie(CONSENT_COOKIE_NAME);
|
|
347
|
+
window.addEventListener?.("focus", recheckStoredChoice);
|
|
348
|
+
document.addEventListener?.("visibilitychange", recheckStoredChoice);
|
|
349
|
+
if (typeof BroadcastChannel !== "function")
|
|
350
|
+
return;
|
|
351
|
+
try {
|
|
352
|
+
consentChannel = new BroadcastChannel(CONSENT_CHANNEL_NAME);
|
|
353
|
+
consentChannel.onmessage = recheckStoredChoice;
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// coercion-ok: without the channel, other tabs recheck on focus.
|
|
357
|
+
}
|
|
358
|
+
}
|
|
240
359
|
/** Store the visitor's choice for every app on this site, and apply it. */
|
|
241
360
|
export function setConsentChoice(choice) {
|
|
242
361
|
if (typeof document === "undefined")
|
|
243
362
|
return;
|
|
244
363
|
writeChoiceCookie(choice);
|
|
245
364
|
storeChoiceOnServer(choice);
|
|
365
|
+
seenChoice = readCookie(CONSENT_COOKIE_NAME);
|
|
366
|
+
// Only a browser that won't keep the cookie falls back to this page's
|
|
367
|
+
// copy; a cookie the visitor clears later means they're asked again.
|
|
368
|
+
state().pageChoice = seenChoice === null ? choice : null;
|
|
369
|
+
try {
|
|
370
|
+
consentChannel?.postMessage("changed");
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// coercion-ok: other tabs still recheck on focus.
|
|
374
|
+
}
|
|
246
375
|
state().settingsOpen = false;
|
|
247
376
|
if (!state().region)
|
|
248
377
|
state().region = "consent";
|
|
@@ -281,3 +410,4 @@ function drainScriptQueue() {
|
|
|
281
410
|
push(...queued);
|
|
282
411
|
}
|
|
283
412
|
drainScriptQueue();
|
|
413
|
+
watchOtherTabs();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { useOnboarding, type UseOnboardingResult } from "./use-onboarding.js";
|
|
2
|
+
export { useOnboardingChecklist, type UseOnboardingChecklistResult, } from "./use-onboarding-checklist.js";
|
|
2
3
|
export { isFirstRunOnboardingEnabled } from "./first-run-enabled.js";
|
|
3
4
|
export { dispatchFirstRunOnboardingStatus, fetchFirstRunOnboardingStatus, FIRST_RUN_ONBOARDING_STATUS_RESOLVED_EVENT, } from "./first-run-status.js";
|
|
4
5
|
export { useOnboardingPreviewMode, useOnboardingPreviewStep, ONBOARDING_PREVIEW_STORAGE_KEY, ONBOARDING_PREVIEW_QUERY_PARAM, ONBOARDING_PREVIEW_QUERY_VALUE, ONBOARDING_PREVIEW_STEP_QUERY_PARAM, ONBOARDING_PREVIEW_STEPS, getOnboardingPreviewStep, isOnboardingPreviewQuery, } from "./use-preview-mode.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { useOnboarding } from "./use-onboarding.js";
|
|
2
|
+
export { useOnboardingChecklist, } from "./use-onboarding-checklist.js";
|
|
2
3
|
export { isFirstRunOnboardingEnabled } from "./first-run-enabled.js";
|
|
3
4
|
export { dispatchFirstRunOnboardingStatus, fetchFirstRunOnboardingStatus, FIRST_RUN_ONBOARDING_STATUS_RESOLVED_EVENT, } from "./first-run-status.js";
|
|
4
5
|
export { useOnboardingPreviewMode, useOnboardingPreviewStep, ONBOARDING_PREVIEW_STORAGE_KEY, ONBOARDING_PREVIEW_QUERY_PARAM, ONBOARDING_PREVIEW_QUERY_VALUE, ONBOARDING_PREVIEW_STEP_QUERY_PARAM, ONBOARDING_PREVIEW_STEPS, getOnboardingPreviewStep, isOnboardingPreviewQuery, } from "./use-preview-mode.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OnboardingStepStatus } from "../../onboarding/types.js";
|
|
2
|
+
export interface UseOnboardingChecklistResult {
|
|
3
|
+
steps: OnboardingStepStatus[];
|
|
4
|
+
completeCount: number;
|
|
5
|
+
totalCount: number;
|
|
6
|
+
dismissed: boolean;
|
|
7
|
+
collapsed: boolean;
|
|
8
|
+
/** `loading` only before the first answer; a refresh keeps the last one. */
|
|
9
|
+
status: "loading" | "ready" | "error";
|
|
10
|
+
error: Error | null;
|
|
11
|
+
refresh: () => Promise<unknown>;
|
|
12
|
+
dismiss: () => Promise<void>;
|
|
13
|
+
reopen: () => Promise<void>;
|
|
14
|
+
setCollapsed: (collapsed: boolean) => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Reads one named onboarding checklist (steps registered with
|
|
18
|
+
* `registerOnboardingStep({ checklist })`). Rows are re-read whenever a local
|
|
19
|
+
* action or an agent tool finishes, so a step checks itself off after the work
|
|
20
|
+
* that completes it. Dismissal is stored by the onboarding plugin and the
|
|
21
|
+
* collapsed state in application state, so both follow the user across devices.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useOnboardingChecklist(checklist: string): UseOnboardingChecklistResult;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient, } from "@tanstack/react-query";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
import { agentNativePath } from "../api-path.js";
|
|
4
|
+
import { readClientAppState, writeClientAppState, } from "../application-state.js";
|
|
5
|
+
import { useChangeVersions } from "../use-change-version.js";
|
|
6
|
+
const CHECKLIST_TIMEOUT_MS = 10_000;
|
|
7
|
+
function summaryKey(checklist) {
|
|
8
|
+
return ["onboarding-checklist", checklist];
|
|
9
|
+
}
|
|
10
|
+
function collapsedKey(checklist) {
|
|
11
|
+
return ["onboarding-checklist-collapsed", checklist];
|
|
12
|
+
}
|
|
13
|
+
function collapsedStateKey(checklist) {
|
|
14
|
+
return `onboarding:collapsed:${checklist}`;
|
|
15
|
+
}
|
|
16
|
+
function checklistUrl(path, checklist) {
|
|
17
|
+
return agentNativePath(`/_agent-native/onboarding/${path}?checklist=${encodeURIComponent(checklist)}`);
|
|
18
|
+
}
|
|
19
|
+
async function fetchChecklist(checklist, signal) {
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const abort = () => controller.abort();
|
|
22
|
+
signal.addEventListener("abort", abort);
|
|
23
|
+
const timer = setTimeout(abort, CHECKLIST_TIMEOUT_MS);
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(checklistUrl("summary", checklist), {
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok)
|
|
29
|
+
throw new Error(`checklist: ${response.status}`);
|
|
30
|
+
const body = (await response.json());
|
|
31
|
+
return { steps: body.steps, dismissed: !!body.dismissed };
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
signal.removeEventListener("abort", abort);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function postChecklist(path, checklist) {
|
|
39
|
+
const response = await fetch(checklistUrl(path, checklist), {
|
|
40
|
+
method: "POST",
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok)
|
|
43
|
+
throw new Error(`checklist ${path}: ${response.status}`);
|
|
44
|
+
}
|
|
45
|
+
function patchSummary(queryClient, checklist, patch) {
|
|
46
|
+
const snapshot = queryClient.getQueriesData({
|
|
47
|
+
queryKey: summaryKey(checklist),
|
|
48
|
+
});
|
|
49
|
+
queryClient.setQueriesData({ queryKey: summaryKey(checklist) }, (current) => (current ? { ...current, ...patch } : current));
|
|
50
|
+
return snapshot;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Reads one named onboarding checklist (steps registered with
|
|
54
|
+
* `registerOnboardingStep({ checklist })`). Rows are re-read whenever a local
|
|
55
|
+
* action or an agent tool finishes, so a step checks itself off after the work
|
|
56
|
+
* that completes it. Dismissal is stored by the onboarding plugin and the
|
|
57
|
+
* collapsed state in application state, so both follow the user across devices.
|
|
58
|
+
*/
|
|
59
|
+
export function useOnboardingChecklist(checklist) {
|
|
60
|
+
const queryClient = useQueryClient();
|
|
61
|
+
const changeVersion = useChangeVersions(["action"]);
|
|
62
|
+
const summary = useQuery({
|
|
63
|
+
queryKey: [...summaryKey(checklist), changeVersion],
|
|
64
|
+
queryFn: ({ signal }) => fetchChecklist(checklist, signal),
|
|
65
|
+
placeholderData: (previous) => previous,
|
|
66
|
+
});
|
|
67
|
+
const collapsedQuery = useQuery({
|
|
68
|
+
queryKey: collapsedKey(checklist),
|
|
69
|
+
queryFn: async () => {
|
|
70
|
+
const value = await readClientAppState(collapsedStateKey(checklist));
|
|
71
|
+
return value?.collapsed === true;
|
|
72
|
+
},
|
|
73
|
+
staleTime: Infinity,
|
|
74
|
+
});
|
|
75
|
+
const dismissMutation = useMutation({
|
|
76
|
+
mutationFn: (dismissed) => postChecklist(dismissed ? "dismiss" : "reopen", checklist),
|
|
77
|
+
onMutate: (dismissed) => ({
|
|
78
|
+
previous: patchSummary(queryClient, checklist, { dismissed }),
|
|
79
|
+
}),
|
|
80
|
+
onError: (_error, _dismissed, context) => {
|
|
81
|
+
for (const [key, data] of context?.previous ?? []) {
|
|
82
|
+
queryClient.setQueryData(key, data);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
onSettled: () => queryClient.invalidateQueries({ queryKey: summaryKey(checklist) }),
|
|
86
|
+
});
|
|
87
|
+
const collapseMutation = useMutation({
|
|
88
|
+
mutationFn: (collapsed) => writeClientAppState(collapsedStateKey(checklist), { collapsed }),
|
|
89
|
+
onMutate: (collapsed) => {
|
|
90
|
+
const previous = queryClient.getQueryData(collapsedKey(checklist));
|
|
91
|
+
queryClient.setQueryData(collapsedKey(checklist), collapsed);
|
|
92
|
+
return { previous };
|
|
93
|
+
},
|
|
94
|
+
onError: (_error, _collapsed, context) => {
|
|
95
|
+
queryClient.setQueryData(collapsedKey(checklist), context?.previous);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const dismiss = useCallback(() => dismissMutation.mutateAsync(true), [dismissMutation]);
|
|
99
|
+
const reopen = useCallback(() => dismissMutation.mutateAsync(false), [dismissMutation]);
|
|
100
|
+
const setCollapsed = useCallback((collapsed) => collapseMutation.mutateAsync(collapsed).then(() => undefined), [collapseMutation]);
|
|
101
|
+
const steps = summary.data?.steps ?? [];
|
|
102
|
+
return {
|
|
103
|
+
steps,
|
|
104
|
+
completeCount: steps.filter((step) => step.complete).length,
|
|
105
|
+
totalCount: steps.length,
|
|
106
|
+
dismissed: summary.data?.dismissed ?? false,
|
|
107
|
+
collapsed: collapsedQuery.data ?? false,
|
|
108
|
+
status: summary.data ? "ready" : summary.isError ? "error" : "loading",
|
|
109
|
+
error: summary.error,
|
|
110
|
+
refresh: () => queryClient.invalidateQueries({ queryKey: summaryKey(checklist) }),
|
|
111
|
+
dismiss,
|
|
112
|
+
reopen,
|
|
113
|
+
setCollapsed,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -64,7 +64,7 @@ export interface SessionReplayOptions {
|
|
|
64
64
|
}
|
|
65
65
|
export interface SessionReplayStartResult {
|
|
66
66
|
started: boolean;
|
|
67
|
-
reason?: "disabled" | "not-browser" | "missing-public-key" | "missing-session-id" | "missing-user-id" | "sampled-out" | "url-blocked" | "already-active" | "import-failed" | "record-failed";
|
|
67
|
+
reason?: "disabled" | "not-browser" | "missing-public-key" | "missing-session-id" | "missing-user-id" | "sampled-out" | "url-blocked" | "already-active" | "import-failed" | "record-failed" | "consent-required";
|
|
68
68
|
replayId?: string;
|
|
69
69
|
sessionId?: string;
|
|
70
70
|
sampled?: boolean;
|