Skip to content

513 reset processing on error #517

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 23, 2025
Merged
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
5 changes: 5 additions & 0 deletions .changeset/better-cups-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@elbwalker/utils': minor
---

tryCatch with finally [#516](https://github.com/elbwalker/walkerOS/issues/516)
5 changes: 5 additions & 0 deletions .changeset/dry-rockets-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@elbwalker/source-datalayer': minor
---

filter on true [#514](https://github.com/elbwalker/walkerOS/issues/514)
6 changes: 6 additions & 0 deletions .changeset/sour-paths-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@elbwalker/source-datalayer': minor
---

reset processing on error
[#513](https://github.com/elbwalker/walkerOS/issues/513)
5 changes: 5 additions & 0 deletions .changeset/tall-grapes-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@elbwalker/source-datalayer': minor
---

async support [#515](https://github.com/elbwalker/walkerOS/issues/515)
51 changes: 28 additions & 23 deletions packages/sources/datalayer/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -54,26 +54,13 @@ describe('source dataLayer', () => {
expect(dataLayer).toEqual(['foo']);
});

test('push', async () => {
sourceDataLayer({ elb });
dataLayer.push({ event: 'foo' });
await jest.runAllTimersAsync();
expect(elb).toHaveBeenCalledTimes(1);
expect(elb).toHaveBeenCalledWith({
event: 'dataLayer foo',
data: { event: 'foo' },
id: expect.any(String),
source: { type: 'dataLayer' },
});
});

test('filter', async () => {
const mockFn = jest.fn();
sourceDataLayer({
elb,
filter: (event) => {
mockFn(event);
return isObject(event) && event.event !== 'foo';
return isObject(event) && event.event === 'foo';
},
});

@@ -187,11 +174,7 @@ describe('source dataLayer', () => {
);

expect(JSON.stringify(source!.skipped)).toBe(
JSON.stringify([
[{ event: 'loop' }],
[{ event: 'loop' }],
[{ event: 'loop' }],
]),
JSON.stringify([{ event: 'loop' }, { event: 'loop' }, { event: 'loop' }]),
);

expect(loopFn).toHaveBeenCalledTimes(3);
@@ -235,12 +218,34 @@ describe('source dataLayer', () => {
throw new Error();
});

const instance = sourceDataLayer({ elb });
await jest.runAllTimersAsync();
dataLayer.push('foo');
const source = await sourceDataLayer({ elb });
dataLayer.push({ event: 'foo' });
await jest.runAllTimersAsync();

expect(elb).toThrow();
expect(mockOrg).toHaveBeenCalledTimes(1);
expect(instance?.processing).toBe(false);
expect(source?.processing).toBe(false);
});

test('failing filter', async () => {
const filterFn = jest
.fn()
.mockImplementationOnce(() => {
throw new Error();
})
.mockImplementation(() => false);

const source = await sourceDataLayer({ elb, filter: filterFn });
dataLayer.push({ event: 'foo' });

await jest.runAllTimersAsync();
expect(filterFn).toHaveBeenCalledTimes(1);
expect(elb).toHaveBeenCalledTimes(0);

dataLayer.push({ event: 'bar' });
await jest.runAllTimersAsync();
expect(elb).toHaveBeenCalledTimes(1);

expect(source?.processing).toBe(false);
});
});
13 changes: 9 additions & 4 deletions packages/sources/datalayer/src/index.ts
Original file line number Diff line number Diff line change
@@ -4,9 +4,9 @@ import { getDataLayer } from './helper';

export * as SourceDataLayer from './types';

export function sourceDataLayer(
export async function sourceDataLayer(
partialConfig: Partial<Config> = {},
): Config | undefined {
): Promise<Config | undefined> {
const { elb, name, prefix = 'dataLayer', skipped = [] } = partialConfig;
if (!elb) return;

@@ -24,8 +24,13 @@ export function sourceDataLayer(
// Override the original push function to intercept incoming events
intercept(config);

// Process already existing events in the dataLayer (ignore Promise handling)
dataLayer.map((item) => push(config, false, item));
// Process existing events (and only those)
const length = dataLayer.length;
for (let i = 0; i < length; i++) {
await push(config, false, dataLayer[i]);
}

config.processing = false;

return config;
}
8 changes: 4 additions & 4 deletions packages/sources/datalayer/src/mapping.ts
Original file line number Diff line number Diff line change
@@ -99,10 +99,10 @@ export async function objToEvent(
}

// https://developers.google.com/tag-platform/gtagjs/reference
export function gtagToObj(args: IArguments): Array<WalkerOS.AnyObject> {
export function gtagToObj(args: IArguments): void | WalkerOS.AnyObject {
const [command, value, params] = Array.from(args);

if (isObject(command)) return [command];
if (isObject(command)) return command;

let event: string | undefined;
let obj = isObject(params) ? params : {};
@@ -127,8 +127,8 @@ export function gtagToObj(args: IArguments): Array<WalkerOS.AnyObject> {
break;
default:
// Ignore command (like get)
return [];
return;
}

return [{ ...obj, event }];
return { ...obj, event };
}
41 changes: 21 additions & 20 deletions packages/sources/datalayer/src/push.ts
Original file line number Diff line number Diff line change
@@ -28,48 +28,49 @@ export function intercept(config: Config) {
export async function push(config: Config, live: boolean, ...args: unknown[]) {
// Clone the arguments to avoid mutation
const clonedArgs = clone(args);
const entries = getEntries(clonedArgs);
const event = getEvent(clonedArgs);

if (!isObject(event)) return;

// Prevent infinite loops
if (live && config.processing) {
config.skipped?.push(entries);
config.skipped.push(event);
return;
}

// Get busy
config.processing = true;

await Promise.all(
entries.map(async (obj) => {
tryCatchAsync(
async (obj) => {
// Filter out unwanted events
if (config.filter && !(await tryCatchAsync(config.filter)(obj))) return;
if (config.filter && (await config.filter(obj))) return;

// Map the incoming event to a WalkerOS event
const mappedObj = await objToEvent(filterValues(obj), config);

if (mappedObj) {
const { command, event } = mappedObj;

if (command) {
if (command.name)
config.elb(command.name, command.data as Elb.PushData);
} else if (event) {
if (event.event) config.elb(event);
if (command && command.name) {
await config.elb(command.name, command.data as Elb.PushData);
} else if (event && event.event) {
await config.elb(event);
}
}
}),
);

// Finished processing
config.processing = false;
},
undefined, // Error handler
() => {
// Finished processing
config.processing = false;
},
)(event);
}

function getEntries(args: unknown): unknown[] {
if (isObject(args)) return [args]; // dataLayer.push
function getEvent(args: unknown): void | unknown {
if (isObject(args)) return args; // dataLayer.push
if (isArray(args)) {
if (isArguments(args[0])) return gtagToObj(args[0]); // gtag
return args;
return args[0];
}

return [];
}
4 changes: 2 additions & 2 deletions packages/sources/datalayer/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -14,12 +14,12 @@ declare global {
export type DataLayer = Array<unknown>;
export interface Config {
elb: Elb.Fn | WalkerOS.AnyFunction;
filter?: (event: unknown) => boolean;
filter?: (event: unknown) => WalkerOS.PromiseOrValue<boolean>;
mapping?: Mapping;
name?: string;
prefix: string;
processing?: boolean;
skipped?: unknown[];
skipped: unknown[];
}

export interface Mapping {
44 changes: 44 additions & 0 deletions packages/utils/src/__tests__/tryCatch.test.ts
Original file line number Diff line number Diff line change
@@ -16,6 +16,26 @@ describe('tryCatch', () => {
expect(onError).toHaveBeenCalledWith(expect.any(Error));
});

test('tryCatch with finally', () => {
const onFinally = jest.fn();
const onError = jest.fn();

// Test finally with successful execution
tryCatch(() => 'success', undefined, onFinally)();
expect(onFinally).toHaveBeenCalledTimes(1);

// Test finally with error
tryCatch(
() => {
throw new Error('foo');
},
onError,
onFinally,
)();
expect(onFinally).toHaveBeenCalledTimes(2);
expect(onError).toHaveBeenCalledWith(expect.any(Error));
});

test('tryCatchAsync', async () => {
const result =
(await tryCatchAsync(async () => {
@@ -30,4 +50,28 @@ describe('tryCatch', () => {
}, onError)();
expect(onError).toHaveBeenCalledWith(expect.any(Error));
});

test('tryCatchAsync with finally', async () => {
const onFinally = jest.fn();
const onError = jest.fn();
// Test finally with successful execution
await tryCatchAsync(async () => 'success', undefined, onFinally)();
expect(onFinally).toHaveBeenCalledTimes(1);

// Test finally with error
await tryCatchAsync(
async () => {
throw new Error('foo');
},
onError,
onFinally,
)();
expect(onFinally).toHaveBeenCalledTimes(2);
expect(onError).toHaveBeenCalledWith(expect.any(Error));

// Test finally with async cleanup
const asyncOnFinally = jest.fn().mockResolvedValue(undefined);
await tryCatchAsync(async () => 'success', undefined, asyncOnFinally)();
expect(asyncOnFinally).toHaveBeenCalledTimes(1);
});
});
12 changes: 12 additions & 0 deletions packages/utils/src/core/tryCatch.ts
Original file line number Diff line number Diff line change
@@ -3,21 +3,27 @@
export function tryCatch<P extends unknown[], R, S>(
fn: (...args: P) => R | undefined,
onError: (err: unknown) => S,
onFinally?: () => void,
): (...args: P) => R | S;
export function tryCatch<P extends unknown[], R>(
fn: (...args: P) => R | undefined,
onError?: undefined,
onFinally?: () => void,
): (...args: P) => R | undefined;
// Implementation
export function tryCatch<P extends unknown[], R, S>(
fn: (...args: P) => R | undefined,
onError?: (err: unknown) => S,
onFinally?: () => void,
): (...args: P) => R | S | undefined {
return function (...args: P): R | S | undefined {
try {
return fn(...args);
} catch (err) {
if (!onError) return;
return onError(err);
} finally {
onFinally?.();
}
};
}
@@ -27,21 +33,27 @@ export function tryCatch<P extends unknown[], R, S>(
export function tryCatchAsync<P extends unknown[], R, S>(
fn: (...args: P) => R,
onError: (err: unknown) => S,
onFinally?: () => void | Promise<void>,
): (...args: P) => Promise<R | S>;
export function tryCatchAsync<P extends unknown[], R>(
fn: (...args: P) => R,
onError?: undefined,
onFinally?: () => void | Promise<void>,
): (...args: P) => Promise<R | undefined>;
// Implementation
export function tryCatchAsync<P extends unknown[], R, S>(
fn: (...args: P) => R,
onError?: (err: unknown) => S,
onFinally?: () => void | Promise<void>,
): (...args: P) => Promise<R | S | undefined> {
return async function (...args: P): Promise<R | S | undefined> {
try {
return await fn(...args);
} catch (err) {
if (!onError) return;
return await onError(err);
} finally {
await onFinally?.();
}
};
}
20 changes: 12 additions & 8 deletions website/docs/sources/dataLayer/configuration.mdx
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ There are a few configuration options when creating a dataLayer source instance:
```ts
import { sourceDataLayer } from '@elbwalker/source-datalayer';

sourceDataLayer({
await sourceDataLayer({
elb: elb, // The elb function to use (required)
filter: (event) => {
// Filter out internal GTM events
@@ -39,7 +39,7 @@ sourceDataLayer({
| Option | Type | Description |
| ------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| elb\* | function | The function to push the events to |
| filter | function | A check to filter specific events |
| filter | function | A check to filter specific events (return `true` to abort processing) |
| mapping | object | The&nbsp;<Link to="/docs/destinations/event_mapping#eventconfig">mapping configuration</Link> of the events |
| name | string | Name of the dataLayer array (default: `dataLayer`) |
| prefix | string | Prefix for the event name to match entity action format (default: `dataLayer`) |
@@ -56,7 +56,7 @@ gtag('set', 'campaign', { term: 'running+shoes' });
// Will become "set campaign" as event name
```

:::tip
:::note

Use `console.log` for the `elb` function to inspect events.

@@ -71,7 +71,7 @@ command and call `elb(name, data)` with the two parameters `name` and `data`.
smallText={true}
labelInput="Configuration"
disableInput={true}
input={`sourceDataLayer({
input={`await sourceDataLayer({
elb,
mapping: {
'consent update': {
@@ -110,8 +110,12 @@ walker.js instance, another dataLayer related destination might push that event
to the dataLayer again (like GA4). Therefore, while processing an event, newly
pushed events are ignored and stored in the `skipped` array.

As a additional rule the `filter` function can be used to ignore events that
might have been pushed by a walker.js destination.
:::note

The `filter` function can also be used to ignore events that might have been
pushed by a walker.js destination.

:::

## Examples

@@ -121,7 +125,7 @@ might have been pushed by a walker.js destination.
smallText={true}
labelInput="Configuration"
disableInput={true}
input={`sourceDataLayer({
input={`await sourceDataLayer({
elb,
mapping: {
add_to_cart: {
@@ -188,7 +192,7 @@ might have been pushed by a walker.js destination.
smallText={true}
labelInput="Configuration"
disableInput={true}
input={`sourceDataLayer({
input={`await sourceDataLayer({
elb,
mapping: {
purchase: {

Unchanged files with check annotations Beta

);
});
test.skip('push', async () => {

Check warning on line 56 in packages/destinations/node/meta/src/__tests__/index.test.ts

GitHub Actions / Build and test

Disabled test
await destination.push(event, config);
expect(mockXHRSend).toHaveBeenCalledWith(expect.any(String));
);
});
test.skip('testCode', async () => {

Check warning on line 67 in packages/destinations/node/meta/src/__tests__/index.test.ts

GitHub Actions / Build and test

Disabled test
config.custom.testCode = 'TESTNNNNN';
await destination.push(event, config);
);
});
test.skip('IDs', async () => {

Check warning on line 78 in packages/destinations/node/meta/src/__tests__/index.test.ts

GitHub Actions / Build and test

Disabled test
event.data.fbclid = 'abc...';
await destination.push(event, config);
expect(user_data.fbc).toContain('abc...');
});
test.skip('user data', async () => {

Check warning on line 95 in packages/destinations/node/meta/src/__tests__/index.test.ts

GitHub Actions / Build and test

Disabled test
event.user.email = 'a@b.c';
event.user.phone = '0401337';
event.user.city = 'Hamburg';
);
});
test.skip('Mapping', async () => {

Check warning on line 121 in packages/destinations/node/meta/src/__tests__/index.test.ts

GitHub Actions / Build and test

Disabled test
event.data = { id: 'abc', quantity: 42, total: 9001 };
const custom: CustomEvent = {
currency: { value: 'EUR' },