Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
62 changes: 62 additions & 0 deletions packages/sdks/src/functions/content-variants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { getCookieSync, setCookie } from '../helpers/cookie.js';

export const testCookiePrefix = 'builder.tests';

export function getTestCookie(name: string) {
return getCookieSync({
name: `${testCookiePrefix}.${name}`,
canTrack: true,
});
}

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({
name: `${testCookiePrefix}.${contentId}`,
value: variationId,
expires: future,
canTrack: true,
});
}
}

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

}
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
94 changes: 94 additions & 0 deletions packages/sdks/src/functions/global-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Global Builder context singleton to store and retrieve Builder configuration
* across the application without prop drilling.
*/

export interface GlobalBuilderContext {
apiKey?: string;
apiHost?: string;
contentId?: string;
}

/**
* Singleton instance to store the global Builder context
*/
class BuilderGlobalContext {
private static instance: BuilderGlobalContext;
private context: GlobalBuilderContext = {};

private constructor() {}

/**
* Get the singleton instance
*/
public static getInstance(): BuilderGlobalContext {
if (!BuilderGlobalContext.instance) {
BuilderGlobalContext.instance = new BuilderGlobalContext();
}
return BuilderGlobalContext.instance;
}

/**
* Set the global context values
*/
public setContext(context: GlobalBuilderContext): void {
this.context = { ...this.context, ...context };
}

/**
* Get the current global context
*/
public getContext(): GlobalBuilderContext {
return { ...this.context };
}

/**
* Clear the global context
*/
public clearContext(): void {
this.context = {};
}

/**
* Get a specific value from the context
*/
public getValue<K extends keyof GlobalBuilderContext>(
key: K
): GlobalBuilderContext[K] {
return this.context[key];
}
}

/**
* Set the global Builder context
* @param context - The context values to set
*/
export function setGlobalBuilderContext(context: GlobalBuilderContext): void {
BuilderGlobalContext.getInstance().setContext(context);
}

/**
* Get the global Builder context
* @returns The current global Builder context
*/
export function getGlobalBuilderContext(): GlobalBuilderContext {
return BuilderGlobalContext.getInstance().getContext();
}

/**
* Get a specific value from the global Builder context
* @param key - The key to retrieve
* @returns The value for the specified key
*/
export function getGlobalBuilderValue<K extends keyof GlobalBuilderContext>(
key: K
): GlobalBuilderContext[K] {
return BuilderGlobalContext.getInstance().getValue(key);
}

/**
* Clear the global Builder context
*/
export function clearGlobalBuilderContext(): void {
BuilderGlobalContext.getInstance().clearContext();
}
Loading