Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 64 additions & 11 deletions app/client/src/utils/bootPylon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function isPylonChatAvailable(): boolean {
// is fully populated. Pylon reads chat_settings at SDK init time and exposes
// no API to update identity post-init, so we must delay script loading until
// the authenticated user's email_hash is on window.pylon.
function injectPylonScriptOnce(appId: string) {
function injectPylonScriptOnce(appId: string, onReady?: () => void) {
const src = `https://widget.usepylon.com/widget/${appId}`;

// Guard against duplicate injection (HMR, multiple boot callers, etc.) by
Expand All @@ -33,11 +33,63 @@ function injectPylonScriptOnce(appId: string) {
script.type = "text/javascript";
script.async = true;
script.src = src;

// Pylon defines window.Pylon during script execution, so onReady (fired on
// the load event) is the earliest point custom fields can be attached. This
// lets us set issue metadata once at SDK boot, so it applies to issues
// created from ANY entry point — not just the Help-menu path that calls
// updatePylonChatIdentity.
if (onReady) {
script.addEventListener("load", onReady);
}

const firstScript = document.getElementsByTagName("script")[0];

firstScript.parentNode?.insertBefore(script, firstScript);
}

// Attach version/build/deployment metadata to every new Pylon issue. All values
// come from getAppsmithConfigs() (no user needed), so this can run at SDK boot
// to cover all entry points. instanceId is only available on the Help-menu path
// (updatePylonChatIdentity), so it is passed in there and omitted at boot.
//
// NOTE: each key below must exist as a matching custom-field *slug* in the
// Pylon dashboard, or Pylon silently drops the value (no client-side error).
// appsmith_edition intentionally duplicates the edition embedded in
// appsmith_version — the latter is a human-readable display string, the former
// a discrete, filterable field for support triage.
// Remember the instanceId once updatePylonChatIdentity has provided it, so a
// later boot-time call (e.g. if the SDK "load" handler runs after the Help-menu
// path) still carries instance_id. Pylon does not document whether
// setNewIssueCustomFields merges or replaces across calls, so we never rely on a
// prior call's instance_id surviving — we re-send it every time it is known.
let cachedInstanceId: string | undefined;

function setPylonNewIssueCustomFields(instanceId?: string) {
if (instanceId) {
cachedInstanceId = instanceId;
}

if (typeof window.Pylon !== "function") {
return;
}

const { appVersion, cloudHosting } = getAppsmithConfigs();
const effectiveInstanceId = instanceId ?? cachedInstanceId;

window.Pylon("setNewIssueCustomFields", {
appsmith_version: `Appsmith ${
!cloudHosting ? appVersion.edition : ""
} ${appVersion.id}`,
appsmith_edition: appVersion.edition,
build_sha: appVersion.sha,
build_date: appVersion.releaseDate,
deployment_type: cloudHosting ? "cloud" : "self-hosted",
...(effectiveInstanceId ? { instance_id: effectiveInstanceId } : {}),
license_id: getLicenseKey() ?? "",
});
}

export default function bootPylon(user?: User) {
if (!isPylonChatAvailable()) {
return;
Expand Down Expand Up @@ -70,7 +122,9 @@ export default function bootPylon(user?: User) {
// present prevents the widget from booting in unverified mode and then
// getting 401s on message send.
if (emailHash) {
injectPylonScriptOnce(pylonAppID);
// Set issue metadata as soon as the SDK is ready so it covers issues
// created from any entry point, not just the Help menu.
injectPylonScriptOnce(pylonAppID, () => setPylonNewIssueCustomFields());
}
}

Expand All @@ -79,7 +133,7 @@ export const updatePylonChatIdentity = (instanceId: string, user?: User) => {
return;
}

const { appVersion, cloudHosting, pylonAppID } = getAppsmithConfigs();
const { pylonAppID } = getAppsmithConfigs();

const email: string | undefined = user?.email;
const username =
Expand All @@ -106,14 +160,13 @@ export const updatePylonChatIdentity = (instanceId: string, user?: User) => {
};

if (sdkNotYetLoaded) {
injectPylonScriptOnce(pylonAppID);
injectPylonScriptOnce(pylonAppID, () =>
setPylonNewIssueCustomFields(instanceId),
);
}

window.Pylon("setNewIssueCustomFields", {
appsmith_version: `Appsmith ${
!cloudHosting ? appVersion.edition : ""
} ${appVersion.id}`,
instance_id: instanceId,
license_id: getLicenseKey() ?? "",
});
// On this path the SDK is already loaded (gated by isPylonChatAvailable), so
// set the fields directly and include instance_id, which is only available
// here.
setPylonNewIssueCustomFields(instanceId);
};
Loading