Skip to content

Commit 4ffe150

Browse files
Merge pull request #130 from trycompai/main
release: v1.11.0
2 parents fc0c594 + 61f8646 commit 4ffe150

7 files changed

Lines changed: 76 additions & 23 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "1.10.0"
2+
".": "1.11.0"
33
}

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [1.11.0](https://github.com/trycompai/crm/compare/v1.10.0...v1.11.0) (2026-08-11)
4+
5+
6+
### Features
7+
8+
* **app:** copy the tracking snippet for the selected install method ([#128](https://github.com/trycompai/crm/issues/128)) ([30e0137](https://github.com/trycompai/crm/commit/30e01377781559375c3a58ada50b63016dea7d57))
9+
310
## [1.10.0](https://github.com/trycompai/crm/compare/v1.9.0...v1.10.0) (2026-08-11)
411

512

apps/api/src/tracking/tracking.controller.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import {
1515
Param,
1616
Post,
1717
Req,
18+
Res,
1819
ServiceUnavailableException,
1920
} from "@nestjs/common";
2021
import { ConfigService } from "@nestjs/config";
2122
import { AllowAnonymous } from "@thallesp/nestjs-better-auth";
23+
import type { Response } from "express";
2224
import type { EnvironmentVariables } from "../config/env.validation";
2325
import { InjectDatabase } from "../database/database.constants";
2426
import { TrackingConfigService } from "./tracking-config.service";
@@ -59,9 +61,12 @@ export class TrackingController {
5961
@HttpCode(204)
6062
async collect(
6163
@Req() request: IncomingMessage,
64+
@Res({ passthrough: true }) response: Response,
6265
@Headers("origin") origin?: string,
6366
@Headers("user-agent") userAgent?: string,
6467
): Promise<void> {
68+
response.setHeader("cross-origin-resource-policy", "cross-origin");
69+
6570
const raw = await read(request, MAX_BODY_BYTES);
6671
if (!raw) return;
6772

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
2+
import type { INestApplication } from "@nestjs/common";
3+
import request from "supertest";
4+
5+
describe("Tracking collector", () => {
6+
let app: INestApplication;
7+
8+
beforeAll(async () => {
9+
const { createApp } = await import("../src/create-app");
10+
11+
app = await createApp();
12+
await app.init();
13+
});
14+
15+
afterAll(async () => {
16+
await app.close();
17+
});
18+
19+
it("answers a beacon from another origin with a cross-origin CORP", async () => {
20+
const response = await request(app.getHttpServer())
21+
.post("/api/t/e")
22+
.set("origin", "https://example.com")
23+
.set("content-type", "text/plain")
24+
.send(JSON.stringify({ siteId: "cmp_unknown", events: [] }));
25+
26+
expect(response.status).toBe(204);
27+
expect(response.headers["cross-origin-resource-policy"]).toBe(
28+
"cross-origin",
29+
);
30+
});
31+
});

apps/app/app/(app)/[slug]/settings/tracking/tracking-script.tsx

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { Label } from "@crm/ui/components/label";
3333
import { StatusIndicator } from "@crm/ui/components/status-indicator";
3434
import { Switch } from "@crm/ui/components/switch";
3535
import { useMutation, useQuery } from "@tanstack/react-query";
36+
import { useState } from "react";
3637
import { toast } from "sonner";
3738
import { useCrmCache } from "@/lib/trpc/cache";
3839
import { useTRPC } from "@/lib/trpc/client";
@@ -41,6 +42,7 @@ export function TrackingScript() {
4142
const trpc = useTRPC();
4243
const cache = useCrmCache();
4344
const tracking = useQuery(trpc.tracking.settings.queryOptions());
45+
const [section, setSection] = useState("html");
4446

4547
const setFlag = useMutation(
4648
trpc.tracking.setFlag.mutationOptions({
@@ -116,18 +118,29 @@ export function TrackingScript() {
116118
</CardDescription>
117119

118120
<CardAction>
119-
<Button size="sm" onClick={() => copy(snippet)} type="button">
121+
<Button
122+
size="sm"
123+
onClick={() =>
124+
copy(section === "gtm" ? tagManagerSnippet : snippet)
125+
}
126+
type="button"
127+
>
120128
<Icon icon={Copy} data-icon="inline-start" />
121129
Copy
122130
</Button>
123131
</CardAction>
124132
</CardHeader>
125133

126134
<CardContent>
127-
<Accordion type="single" collapsible defaultValue="html">
135+
<Accordion
136+
type="single"
137+
collapsible
138+
value={section}
139+
onValueChange={setSection}
140+
>
128141
<AccordionItem value="html">
129142
<AccordionTrigger>Paste it into your HTML</AccordionTrigger>
130-
<AccordionContent>
143+
<AccordionContent className="flex flex-col gap-4">
131144
<pre className="overflow-x-auto rounded-md border bg-muted p-4 font-mono text-code-foreground text-xs/5">
132145
<span className="text-code-accent">{"<script"}</span>
133146
{"\n src="}
@@ -149,7 +162,7 @@ export function TrackingScript() {
149162
<AccordionTrigger>
150163
Add it through Google Tag Manager
151164
</AccordionTrigger>
152-
<AccordionContent>
165+
<AccordionContent className="flex flex-col gap-4">
153166
<pre className="overflow-x-auto rounded-md border bg-muted p-4 font-mono text-code-foreground text-xs/5">
154167
<span className="text-code-accent">{"<script"}</span>
155168
{"\n src="}
@@ -168,23 +181,12 @@ export function TrackingScript() {
168181
off any consent-blocked category you do not need.
169182
</li>
170183
</ol>
171-
<div className="flex items-center justify-between gap-4">
172-
<p className="text-muted-foreground text-xs/relaxed">
173-
Tag Manager drops a{" "}
174-
<span className="font-mono text-foreground">data-site</span>{" "}
175-
attribute when it injects a script, so this form carries the
176-
site ID in the URL instead.
177-
</p>
178-
<Button
179-
size="xs"
180-
variant="outline"
181-
type="button"
182-
onClick={() => copy(tagManagerSnippet)}
183-
>
184-
<Icon icon={Copy} data-icon="inline-start" />
185-
Copy
186-
</Button>
187-
</div>
184+
<p className="text-muted-foreground text-xs/relaxed">
185+
Tag Manager drops a{" "}
186+
<span className="font-mono text-foreground">data-site</span>{" "}
187+
attribute when it injects a script, so this form carries the
188+
site ID in the URL instead.
189+
</p>
188190
</AccordionContent>
189191
</AccordionItem>
190192
</Accordion>

docs/tracking.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ somebody else's page. That imposes rules nothing else in this repo has:
6565
`POST /api/t/e`, anonymous, 204, in `TrackingController`. It answers nothing: a
6666
tracker that could read a response is a tracker whose failures a stranger can probe.
6767

68+
**It is the one route that sets `Cross-Origin-Resource-Policy: cross-origin`**, and
69+
it must. `helmet()` puts `same-origin` on every response, which is right for an API
70+
only its own app calls — but this one is called by a `no-cors` beacon on somebody
71+
else's marketing site, so Chrome blocks the reply with
72+
`ERR_BLOCKED_BY_RESPONSE.NotSameOrigin` and logs a failure under every page view.
73+
The header is set on the response, not switched off in `helmet`, so the exception
74+
stays with the route that needs it.
75+
6876
The gauntlet, in order, in `TrackingIngestService.accept`:
6977

7078
1. **User agent** — the `BOT` pattern.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "crm",
33
"private": true,
44
"license": "MIT",
5-
"version": "1.10.0",
5+
"version": "1.11.0",
66
"scripts": {
77
"prepare": "git rev-parse --git-dir >/dev/null 2>&1 && git config core.hooksPath .githooks || true",
88
"build": "turbo run build",

0 commit comments

Comments
 (0)