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
42 changes: 26 additions & 16 deletions packages/clarity-js/src/data/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export let data: Metadata = null;
export let callbacks: MetadataCallbackOptions[] = [];
export let electron = BooleanFlag.False;
let rootDomain = null;
let consentStatus: ConsentState = null;
let consentStatus: ConsentData = null;
let defaultStatus: ConsentState = { source: ConsentSource.API, ad_Storage: Constant.Denied, analytics_Storage: Constant.Denied };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not be ConsentData type? Maybe also consider giving this a unique source as well (the more the better).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConsentData uses boolean flags, the default status uses strings "granted" | "denied" sent to the API


export function start(): void {
Expand Down Expand Up @@ -90,12 +90,12 @@ export function start(): void {
if (consentStatus === null) {
consentStatus = {
source: ConsentSource.Implicit,
ad_Storage: config.track ? Constant.Granted : Constant.Denied,
analytics_Storage: config.track ? Constant.Granted : Constant.Denied,
ad_Storage: config.track ? BooleanFlag.True : BooleanFlag.False,
analytics_Storage: config.track ? BooleanFlag.True : BooleanFlag.False,
};
}
const consent = getConsentData(consentStatus);
trackConsent.config(consent);

trackConsent.config(consentStatus);
// Track ids using a cookie if configuration allows it
track(u);
}
Expand Down Expand Up @@ -127,7 +127,7 @@ export function metadata(cb: MetadataCallback, wait: boolean = true, recall: boo
// we go through the upgrading flow.
if (data && (upgraded || wait === false)) {
// Immediately invoke the callback if the caller explicitly doesn't want to wait for the upgrade confirmation
cb(data, !config.lean, consentInfo ? consentStatus : undefined);
cb(data, !config.lean, consentInfo ? getConsentState() : undefined);
called = true;
}
if (recall || !called) {
Expand All @@ -147,15 +147,18 @@ export function consent(status = true): void {
}

consentv2({ ad_Storage: Constant.Granted, analytics_Storage: Constant.Granted });
trackConsent.consent();
}

export function consentv2(consentState: ConsentState = defaultStatus, source: number = ConsentSource.API): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this or later PR change to APIV2 so we know more precisely the source.

const updatedStatus = {

const currentState = getConsentState();
const consentUpdate = {
source: consentState.source ?? source,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this alway mean we keep the last source? Should it not be flipped?

ad_Storage: normalizeConsent(consentState.ad_Storage, consentStatus?.ad_Storage),
analytics_Storage: normalizeConsent(consentState.analytics_Storage, consentStatus?.analytics_Storage),
};
ad_Storage: normalizeConsent(consentState.ad_Storage, currentState.ad_Storage),
analytics_Storage: normalizeConsent(consentState.analytics_Storage, currentState.analytics_Storage),
}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon at the end of this object literal. This should be consistent with other object declarations in the codebase.

Suggested change
}
};

Copilot uses AI. Check for mistakes.

const updatedStatus = getConsentData(consentUpdate);

if (
consentStatus &&
Expand All @@ -167,9 +170,8 @@ export function consentv2(consentState: ConsentState = defaultStatus, source: nu

consentStatus = updatedStatus;
callback(true);
const consentData = getConsentData(consentStatus);

if (!consentData.analytics_Storage && config.track) {
if (!consentStatus.analytics_Storage && config.track) {
config.track = false;
setCookie(Constant.SessionKey, Constant.Empty, -Number.MAX_VALUE);
setCookie(Constant.CookieKey, Constant.Empty, -Number.MAX_VALUE);
Expand All @@ -178,13 +180,13 @@ export function consentv2(consentState: ConsentState = defaultStatus, source: nu
return;
}

if (core.active() && consentData.analytics_Storage) {
if (core.active() && consentStatus.analytics_Storage) {
config.track = true;
track(user(), BooleanFlag.True);
save();
}

trackConsent.trackConsentv2(consentData);
trackConsent.trackConsentv2(consentStatus);
trackConsent.consent();
}

Expand All @@ -198,6 +200,14 @@ function getConsentData(consentState: ConsentState): ConsentData {
return consent;
}

function getConsentState(): ConsentState {
return {
source: consentStatus.source ?? ConsentSource.Implicit,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When can source be null/undefined?

ad_Storage: consentStatus.ad_Storage === BooleanFlag.True ? Constant.Granted : Constant.Denied,
analytics_Storage: consentStatus.analytics_Storage === BooleanFlag.True ? Constant.Granted : Constant.Denied,
};
Comment on lines +203 to +208
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null reference error: consentStatus can be null, but this function accesses its properties without null checking. This function is called from line 154 in consentv2() before consentStatus is guaranteed to be initialized. Consider adding a null check or defaulting to implicit consent values when consentStatus is null:

function getConsentState(): ConsentState {
  if (!consentStatus) {
    return {
      source: ConsentSource.Implicit,
      ad_Storage: Constant.Denied,
      analytics_Storage: Constant.Denied
    };
  }
  return {
    source: consentStatus.source ?? ConsentSource.Implicit,
    ad_Storage: consentStatus.ad_Storage === BooleanFlag.True ? Constant.Granted : Constant.Denied,
    analytics_Storage: consentStatus.analytics_Storage === BooleanFlag.True ? Constant.Granted : Constant.Denied,
  };
}

Copilot uses AI. Check for mistakes.
}
Comment on lines +203 to +209
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new getConsentState() function lacks test coverage. Consider adding unit tests to verify:

  1. Correct conversion from BooleanFlag to Constant.Granted/Denied
  2. Proper handling of null consentStatus
  3. Default source value when consentStatus.source is undefined

This is especially important given this function is used in callback flows (lines 130, 252) and in consentv2() (line 154).

Copilot uses AI. Check for mistakes.

function normalizeConsent(value: unknown, fallback: string = Constant.Denied): string {
return typeof value === 'string' ? value.toLowerCase() : fallback;
}
Expand Down Expand Up @@ -239,7 +249,7 @@ function processCallback(upgrade: BooleanFlag, consentUpdate: boolean = false):
((!cb.called && !consentUpdate) || (cb.consentInfo && consentUpdate)) && //If consentUpdate is true, we only call the callback if it has consentInfo
(!cb.wait || upgrade)
) {
cb.callback(data, !config.lean, cb.consentInfo ? consentStatus : undefined);
cb.callback(data, !config.lean, cb.consentInfo ? getConsentState() : undefined);
cb.called = true;
if (!cb.recall) {
callbacks.splice(i, 1);
Expand Down