Skip to content

Commit 6a4946a

Browse files
authored
feat(ads): extend conversion tracking to Messenger and Instagram (#1035)
1 parent 53d4165 commit 6a4946a

193 files changed

Lines changed: 167123 additions & 1504 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ See **`.agents/rules/git.md`** for the full canonical rules (commit format, bran
186186
- Tech stack details: `docs/tech-stack.md`
187187
- Request flow diagrams: `docs/request-workflow.md`
188188
- White-label tenancy model: `docs/tenancy.md`
189+
- Ads conversion tracking (CTWA/CTM/CTID, rules vs Trigger actions, CAPI): `docs/ads-conversion-tracking.md`
189190
- Facebook comment automation: `docs/fb-comment-automation.md` (skill: `.agents/skills/fb-comment-automation/`)
190191
- Push notifications (Expo Push Service, device tokens): `docs/push-notifications.md`
191192
- Enterprise licensing (offline Ed25519 license keys): `docs/licensing.md`
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, test } from "vitest"
2+
import {
3+
adsAnalyticsChannelValues,
4+
adsAnalyticsSearchParamsCache,
5+
} from "@/features/ads/schemas/analytics"
6+
7+
describe("Ads Analytics channel filter — 'All channels' default", () => {
8+
test("adsAnalyticsChannelValues includes every ads-eligible channel plus the 'all' sentinel", () => {
9+
expect(adsAnalyticsChannelValues).toEqual(
10+
expect.arrayContaining(["whatsapp", "messenger", "instagram", "all"]),
11+
)
12+
expect(adsAnalyticsChannelValues).toHaveLength(4)
13+
})
14+
15+
test("channel defaults to 'all' when the URL param is omitted", async () => {
16+
const parsed = await adsAnalyticsSearchParamsCache.parse({})
17+
18+
expect(parsed.channel).toBe("all")
19+
})
20+
21+
test("a legacy ?channel=whatsapp deep link still pins WhatsApp (not overridden by the new default)", async () => {
22+
const parsed = await adsAnalyticsSearchParamsCache.parse({
23+
channel: "whatsapp",
24+
})
25+
26+
expect(parsed.channel).toBe("whatsapp")
27+
})
28+
29+
test("a legacy ?channel=messenger deep link still pins Messenger", async () => {
30+
const parsed = await adsAnalyticsSearchParamsCache.parse({
31+
channel: "messenger",
32+
})
33+
34+
expect(parsed.channel).toBe("messenger")
35+
})
36+
37+
test("an explicit ?channel=all round-trips to 'all'", async () => {
38+
const parsed = await adsAnalyticsSearchParamsCache.parse({
39+
channel: "all",
40+
})
41+
42+
expect(parsed.channel).toBe("all")
43+
})
44+
})
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// @vitest-environment jsdom
2+
3+
import { act, type ReactNode } from "react"
4+
import { createRoot, type Root } from "react-dom/client"
5+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"
6+
import { ChannelFilter } from "@/features/ads/components/channel-filter"
7+
import type { AdsAnalyticsSearchParams } from "@/features/ads/schemas/analytics"
8+
9+
vi.mock("next/navigation", () => ({
10+
usePathname: () => "/space/ws-1/dashboard/ads",
11+
useRouter: () => ({ push: vi.fn() }),
12+
useSearchParams: () => new URLSearchParams(),
13+
}))
14+
15+
vi.mock("next-intl", () => ({
16+
useTranslations: () => (key: string) => key,
17+
}))
18+
19+
vi.mock("@chatbotx.io/ui/components/ui/select", () => ({
20+
Select: ({ children }: { children: ReactNode }) => children,
21+
SelectContent: ({ children }: { children: ReactNode }) => children,
22+
SelectItem: ({ children }: { children: ReactNode }) => (
23+
<div data-select-item>{children}</div>
24+
),
25+
SelectTrigger: ({ children, id }: { children: ReactNode; id?: string }) => (
26+
<div data-select-trigger={id}>{children}</div>
27+
),
28+
SelectValue: () => null,
29+
}))
30+
31+
const baseRange = {
32+
from: "2026-08-01",
33+
to: "2026-08-10",
34+
account: "",
35+
channelAccount: "",
36+
adAccount: "",
37+
} as Omit<AdsAnalyticsSearchParams, "channel">
38+
39+
describe("ChannelFilter — 'All channels' option and integration-select visibility", () => {
40+
let container: HTMLDivElement
41+
let root: Root
42+
43+
beforeEach(() => {
44+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
45+
container = document.createElement("div")
46+
document.body.append(container)
47+
root = createRoot(container)
48+
})
49+
50+
afterEach(() => {
51+
act(() => {
52+
root.unmount()
53+
})
54+
container.remove()
55+
})
56+
57+
test("renders an 'All channels' option via the special label branch, not a fake tabs.all key", async () => {
58+
await act(async () => {
59+
root.render(
60+
<ChannelFilter
61+
channelIntegrations={[]}
62+
range={{ ...baseRange, channel: "all" }}
63+
selectedIntegrationId={null}
64+
/>,
65+
)
66+
await Promise.resolve()
67+
})
68+
69+
expect(container.textContent).toContain(
70+
"ads.analytics.channelFilter.allChannels",
71+
)
72+
expect(container.textContent).not.toContain("ads.conversionEvents.tabs.all")
73+
})
74+
75+
test("hides the integration select entirely when channel is 'all'", async () => {
76+
await act(async () => {
77+
root.render(
78+
<ChannelFilter
79+
channelIntegrations={[{ id: "msg-1", name: "My Page" }]}
80+
range={{ ...baseRange, channel: "all" }}
81+
selectedIntegrationId={null}
82+
/>,
83+
)
84+
await Promise.resolve()
85+
})
86+
87+
expect(
88+
container.querySelector(
89+
'[data-select-trigger="ads-analytics-channel-account"]',
90+
),
91+
).toBeNull()
92+
})
93+
94+
test("shows the integration select for a concrete channel", async () => {
95+
await act(async () => {
96+
root.render(
97+
<ChannelFilter
98+
channelIntegrations={[{ id: "msg-1", name: "My Page" }]}
99+
range={{ ...baseRange, channel: "messenger" }}
100+
selectedIntegrationId="msg-1"
101+
/>,
102+
)
103+
await Promise.resolve()
104+
})
105+
106+
expect(
107+
container.querySelector(
108+
'[data-select-trigger="ads-analytics-channel-account"]',
109+
),
110+
).not.toBeNull()
111+
})
112+
})

0 commit comments

Comments
 (0)