Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
14 changes: 14 additions & 0 deletions .changeset/wild-ravens-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@builder.io/react": patch
"@builder.io/sdk": patch
"@builder.io/sdk-angular": patch
"@builder.io/sdk-react-nextjs": patch
"@builder.io/sdk-qwik": patch
"@builder.io/sdk-react": patch
"@builder.io/sdk-react-native": patch
"@builder.io/sdk-solid": patch
"@builder.io/sdk-svelte": patch
"@builder.io/sdk-vue": patch
---

fix: handle conversion tracking for gen1 and gen2 sdks
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"lint:fix": "prettier --write '**/*.{js,jsx,ts,tsx}'",
"update-npm-dependency": "zx ./scripts/update-npm-dependency.mjs",
"g:changeset": "changeset",
"g:nx": "cd $INIT_CWD && nx"
"g:nx": "cd $INIT_CWD && nx",
"watch:sdk": "npx watch \"yarn g:nx build $0\" packages/sdks/src packages/sdks/overrides"
},
"engines": {
"yarn": ">= 3.0.0"
Expand Down
34 changes: 32 additions & 2 deletions packages/core/src/builder.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,7 @@ export class Builder {
private hasOverriddenCanTrack = false;
private apiKey$ = new BehaviorSubject<string | null>(null);
private authToken$ = new BehaviorSubject<string | null>(null);
private contentId$ = new BehaviorSubject<string | null>(null);

userAttributesChanged = new BehaviorSubject<any>(null);

Expand Down Expand Up @@ -1601,9 +1602,30 @@ export class Builder {
return;
}
const meta = typeof contentId === 'object' ? contentId : customProperties;
const useContentId = typeof contentId === 'string' ? contentId : undefined;
let useContentId = typeof contentId === 'string' ? contentId : undefined;

this.track('conversion', { amount, variationId, meta, contentId: useContentId }, context);
if (!useContentId && this.contentId) {
useContentId = this.contentId;
}

let useVariationId = variationId;
if (!useVariationId && useContentId) {
useVariationId = this.getTestCookie(useContentId);
}

this.track(
'conversion',
{
amount,
variationId:
useVariationId && useContentId && useVariationId !== useContentId
? useVariationId
: undefined,
meta,
contentId: useContentId,
},
context
);
}

autoTrack = !Builder.isBrowser
Expand Down Expand Up @@ -1708,6 +1730,14 @@ export class Builder {
this.apiKey$.next(key);
}

get contentId() {
return this.contentId$.value;
}

set contentId(id: string | null) {
this.contentId$.next(id);
}

get authToken() {
return this.authToken$.value;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/components/builder-component.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1374,6 +1374,9 @@ export class BuilderComponent extends React.Component<
}

onContentLoaded = (data: any, content: Content) => {
if (content && content.id) {
this.builder.contentId = content.id;
}
if (this.name === 'page' && Builder.isBrowser) {
if (data) {
const { title, pageTitle, description, pageDescription } = data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export class BuilderContent<ContentType extends object = any> extends React.Comp
const contentData = this.options.initialContent[0];
// TODO: intersectionobserver like in subscribetocontent - reuse the logic
if (contentData?.id) {
this.builder.contentId = contentData.id;
this.builder.trackImpression(contentData.id, this.renderedVariantId, undefined, {
content: contentData,
});
Expand Down
13 changes: 13 additions & 0 deletions packages/sdks/docs/DEVELOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ You can fetch real data from the Builder API instead of using the JSON mock file

To unlink the SDK from your project, all you have to do is run `npm install` in your project folder. That will clear all sym-links.

*Alternative Method* :

- Build the SDK using `yarn g:nx build @builder.io/sdk-react` (replace `react` with the SDK you want to build)
- In your project temporarily replace the sdk dependency as follows:

```
"@builder.io/sdk-react": "link:<absolute-path-to-project-directory>/packages/sdks/output/react",
```
- `yarn install` in your project folder
- If you want your sdk to refresh when making changes in the `packages/sdks/src` folder, run the command in root folder `yarn run watch:sdk @builder.io/sdk-react` (replace `react` with the SDK you want to build)

This achieves the same result as the npm link method above, but uses yarn instead.

**NOTE: Testing React-Native SDK in iOS Simulator**

One big caveat is that the iOS Simulator does not support sym-linked packages. To workaround this, you will have to copy the SDK folder. This means that you will need to manually do so every time you want a new change to be reflected. in the react-native example, there is a handy `yarn run cp-sdk` command to do that for you.
Expand Down
13 changes: 13 additions & 0 deletions packages/sdks/src/components/content/content.lite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import type {
BuilderRenderState,
RegisteredComponents,
} from '../../context/types.js';
import { setTestsFromUrl } from '../../functions/content-variants.js';
import { evaluate } from '../../functions/evaluate/evaluate.js';
import { setGlobalBuilderContext } from '../../functions/global-context.js';
import { isBrowser } from '../../functions/is-browser.js';
import { serializeIncludingFunctions } from '../../functions/register-component.js';
import { logger } from '../../helpers/logger.js';
import type { ComponentInfo } from '../../types/components.js';
Expand Down Expand Up @@ -139,6 +142,16 @@ export default function ContentComponent(props: ContentProps) {
);
}

setGlobalBuilderContext({
apiKey: props.apiKey,
apiHost: props.apiHost,
contentId: builderContextSignal.value.content?.id,
});

if (isBrowser()) {
setTestsFromUrl();
}

// run any dynamic JS code attached to content
const jsCode = builderContextSignal.value.content?.data?.jsCode;

Expand Down
54 changes: 54 additions & 0 deletions packages/sdks/src/functions/content-variants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { getCookie, setCookie } from './cookie.js';

export const testCookiePrefix = 'builder.tests';

export function getTestCookie(name: string) {
return getCookie(`${testCookiePrefix}.${name}`);
}

function parseUrlParams(url: string): Map<string, string> {
const result = new Map<string, string>();

try {
const urlObj = new URL(url);
const params = urlObj.searchParams;

for (const [key, value] of params) {
result.set(key, value);
}
} catch (error) {
console.debug('Error parsing URL parameters:', error);
}

return result;
}

export function setTestCookie(contentId: string, variationId: string) {
// 30 days from now
const future = new Date();
future.setDate(future.getDate() + 30);

// Use the native setCookie function directly
if (typeof window !== 'undefined') {
setCookie(`${testCookiePrefix}.${contentId}`, variationId, future);
}
}

export function setTestsFromUrl() {
if (typeof window === 'undefined') return;

try {
// Use native URL object to parse current page URL
const params = parseUrlParams(window.location.href);

// Look for parameters that start with 'builder.tests.'
for (const [key, value] of params) {
if (key.startsWith(`${testCookiePrefix}.`)) {
const testKey = key.replace(`${testCookiePrefix}.`, '');
setTestCookie(testKey, value);
}
}
} catch (e) {
console.debug('Error parsing tests from URL', e);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

all of these functions are not used anymore right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, not used anymore. Thanks for catching. removed the unused functions

}
43 changes: 43 additions & 0 deletions packages/sdks/src/functions/cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { isBrowser } from './is-browser.js';

export function setCookie(name: string, value: string, expires?: Date) {
try {
let expiresString = '';

// TODO: need to know if secure server side
if (expires) {
expiresString = '; expires=' + expires.toUTCString();
}

const secure = isBrowser() ? location.protocol === 'https:' : true;
document.cookie =
name +
'=' +
(value || '') +
expiresString +
'; path=/' +
(secure ? '; secure; SameSite=None' : '');
} catch (err) {
console.warn('Could not set cookie', err);
}
}

export function getCookie(name: string) {
try {
return (
decodeURIComponent(
document.cookie.replace(
new RegExp(
'(?:(?:^|.*;)\\s*' +
encodeURIComponent(name).replace(/[-.+*]/g, '\\$&') +
'\\s*\\=\\s*([^;]*).*$)|^.*$'
),
'$1'
)
) || null
);
} catch (err) {
console.warn('Could not get cookie', err);
return null;
}
}
67 changes: 67 additions & 0 deletions packages/sdks/src/functions/evaluate/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import type {
BuilderContextInterface,
BuilderRenderState,
} from '../../context/types.js';
import { getDefaultCanTrack } from '../../helpers/canTrack.js';
import { getTestCookie } from '../content-variants.js';
import { getGlobalBuilderContext } from '../global-context.js';
import { isBrowser } from '../is-browser.js';
import { isEditing } from '../is-editing.js';
import { getUserAttributes } from '../track/helpers.js';
import type { EventProps } from '../track/index.js';
import { _track } from '../track/index.js';

export type EvaluatorArgs = Omit<ExecutorArgs, 'builder' | 'event'> & {
event?: Event;
Expand All @@ -15,6 +20,18 @@ export type BuilderGlobals = {
isBrowser: boolean | undefined;
isServer: boolean | undefined;
getUserAttributes: typeof getUserAttributes;
track: (
eventName: string,
properties: Partial<EventProps & { apiHost?: string }>,
context?: any
) => void;
trackConversion: (
amount?: number,
contentId?: string | any,
variationId?: string,
customProperties?: any,
context?: any
) => void;
};

export type ExecutorArgs = Pick<
Expand Down Expand Up @@ -52,6 +69,56 @@ export const getBuilderGlobals = (): BuilderGlobals => ({
isBrowser: isBrowser(),
isServer: !isBrowser(),
getUserAttributes: () => getUserAttributes(),
track: (
eventName: string,
properties: Partial<EventProps & { apiHost?: string }> = {},
context?: any
) => {
const builderContext = getGlobalBuilderContext();
_track({
type: eventName,
...properties,
apiHost: builderContext?.apiHost,
apiKey: builderContext?.apiKey || '',
context,
canTrack: getDefaultCanTrack(properties.canTrack),
});
},
trackConversion: (
amount?: number,
contentId?: string,
variationId?: string,
customProperties?: any,
context?: any
) => {
const meta = typeof contentId === 'object' ? contentId : customProperties;
let useContentId = typeof contentId === 'string' ? contentId : undefined;
const builderContext = getGlobalBuilderContext();

if (!useContentId && builderContext?.contentId) {
useContentId = builderContext.contentId;
}

let useVariationId = variationId;
if (!useVariationId && useContentId) {
useVariationId = getTestCookie(useContentId) || undefined;
}

_track({
type: 'conversion',
apiHost: builderContext?.apiHost,
apiKey: builderContext?.apiKey || '',
amount: amount || undefined,
Copy link

Choose a reason for hiding this comment

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

Bug: Zero Amounts Falsely Converted to Undefined

The trackConversion function incorrectly converts a 0 amount to undefined because amount || undefined treats 0 as falsy. This prevents valid zero-amount conversions from being tracked as intended.

Fix in Cursor Fix in Web

contentId: useContentId,
variationId:
useVariationId && useContentId && useVariationId !== useContentId
? useVariationId
: undefined,
meta,
context: context || undefined,
canTrack: getDefaultCanTrack(),
});
},
});

export const parseCode = (
Expand Down
Loading
Loading