Skip to content

Commit 950f643

Browse files
committed
Fix GDPR consent API endpoint URL
1 parent 77bcaa2 commit 950f643

5 files changed

Lines changed: 100 additions & 84 deletions

File tree

‎.cache/pages.json‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,28 +103,36 @@
103103
"apis-and-gateways",
104104
"argo-cd",
105105
"aws",
106+
"aws/2",
107+
"aws/3",
106108
"azure",
107109
"backstage-idp",
108110
"build-and-deploy",
109111
"cloud-platforms",
112+
"cloud-platforms/2",
110113
"crossplane",
111114
"docker",
112115
"dotnet",
113116
"go",
114117
"grafana",
115118
"helm",
116119
"kubernetes",
120+
"kubernetes/2",
121+
"kubernetes/3",
117122
"observability-and-telemetry",
118123
"openstack",
119124
"platform-engineering",
120125
"prometheus",
126+
"prometheus/2",
121127
"python",
128+
"python/2",
122129
"reliability-and-testing",
123130
"ruby",
124131
"system-modernization",
125132
"systems-and-development",
126133
"terraform",
127-
"typescript"
134+
"typescript",
135+
"typescript/2"
128136
]
129137
},
130138
"terms"

‎src/components/scripts/sentry/__tests__/helpers.spec.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const createContactSubmitHttpErrorEvent = (): Parameters<typeof beforeSendHandle
5757
const createConsentRateLimitHttpErrorEvent = (): Parameters<typeof beforeSendHandler>[0] =>
5858
({
5959
type: 'error',
60-
request: { url: 'https://www.webstackbuilders.com/_actions/gdpr.consentCreate' },
60+
request: { url: 'https://www.webstackbuilders.com/_actions/gdpr/consentCreate' },
6161
exception: {
6262
values: [
6363
{

‎src/components/scripts/sentry/helpers.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ type BeforeSendHandler = NonNullable<BrowserOptions['beforeSend']>
66

77
const SAFE_BREADCRUMB_CATEGORIES = new Set(['script', 'sentry.event'])
88

9+
const isConsentActionRequest = (requestUrl: string): boolean => {
10+
return (
11+
requestUrl.includes('/_actions/gdpr.consentCreate') ||
12+
requestUrl.includes('/_actions/gdpr/consentCreate')
13+
)
14+
}
15+
916
const isHandledContactSubmitHttpError = (event: Parameters<BeforeSendHandler>[0]): boolean => {
1017
const requestUrl = event.request?.url
1118
const exception = event.exception?.values?.[0]
@@ -29,7 +36,7 @@ const isHandledConsentRateLimitHttpError = (event: Parameters<BeforeSendHandler>
2936

3037
return (
3138
typeof requestUrl === 'string' &&
32-
requestUrl.includes('/_actions/gdpr.consentCreate') &&
39+
isConsentActionRequest(requestUrl) &&
3340
mechanismType === 'auto.http.client.fetch' &&
3441
typeof errorMessage === 'string' &&
3542
errorMessage.includes('HTTP Client Error with status code: 429')

‎src/components/scripts/store/__tests__/consent.spec.ts‎

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ import {
2727
import { $isConsentBannerVisible } from '@components/scripts/store/consentBanner'
2828
import * as errorHandlerModule from '@components/scripts/errors/handler'
2929

30+
const consentCreateMock = vi.hoisted(() => vi.fn())
31+
32+
vi.mock('astro:actions', () => ({
33+
actions: {
34+
gdpr: {
35+
consentCreate: consentCreateMock,
36+
},
37+
},
38+
}))
39+
3040
// Mock js-cookie
3141
vi.mock('js-cookie', () => ({
3242
default: {
@@ -81,6 +91,7 @@ describe('Cookie Consent Management', () => {
8191

8292
// Clear mocks
8393
vi.clearAllMocks()
94+
consentCreateMock.mockReset()
8495

8596
// Clear localStorage
8697
localStorage.clear()
@@ -320,8 +331,7 @@ describe('Consent side effects', () => {
320331
})
321332

322333
it('logs consent updates via the GDPR API when preferences change', async () => {
323-
const fetchSpy = vi.fn().mockResolvedValue({ ok: true })
324-
vi.stubGlobal('fetch', fetchSpy)
334+
consentCreateMock.mockResolvedValue({ data: { success: true, record: { id: 'consent-1' } } })
325335

326336
let consentListener:
327337
| ((_state: ConsentState, _oldState?: ConsentState) => Promise<void> | void)
@@ -353,16 +363,13 @@ describe('Consent side effects', () => {
353363
await consentListener?.(newState, oldState)
354364

355365
await vi.waitFor(() => {
356-
expect(fetchSpy).toHaveBeenCalledTimes(1)
366+
expect(consentCreateMock).toHaveBeenCalledTimes(1)
357367
})
358-
const firstFetchCall = fetchSpy.mock.calls.at(0)
359-
if (!firstFetchCall) {
360-
throw new TestError('Expected consent logging fetch to be called once')
368+
const firstConsentCall = consentCreateMock.mock.calls.at(0)
369+
if (!firstConsentCall) {
370+
throw new TestError('Expected consent logging action to be called once')
361371
}
362-
const [url, options] = firstFetchCall
363-
expect(url).toBe('/_actions/gdpr.consentCreate')
364-
expect(options?.method).toBe('POST')
365-
const payload = JSON.parse(options?.body as string)
372+
const [payload] = firstConsentCall
366373
expect(payload).toMatchObject({
367374
DataSubjectId: validDataSubjectId,
368375
purposes: ['analytics'],
@@ -372,13 +379,12 @@ describe('Consent side effects', () => {
372379

373380
await consentListener?.(newState, undefined)
374381
await vi.waitFor(() => {
375-
expect(fetchSpy).toHaveBeenCalledTimes(1)
382+
expect(consentCreateMock).toHaveBeenCalledTimes(1)
376383
})
377384
})
378385

379386
it('regenerates a DataSubjectId before logging when state value is missing or invalid', async () => {
380-
const fetchSpy = vi.fn().mockResolvedValue({ ok: true })
381-
vi.stubGlobal('fetch', fetchSpy)
387+
consentCreateMock.mockResolvedValue({ data: { success: true, record: { id: 'consent-1' } } })
382388

383389
const regeneratedId = 'regenerated-data-subject-id'
384390
vi.mocked(getOrCreateDataSubjectId).mockReturnValue(regeneratedId)
@@ -412,25 +418,23 @@ describe('Consent side effects', () => {
412418
await consentListener?.(newState, oldState)
413419

414420
await vi.waitFor(() => {
415-
expect(fetchSpy).toHaveBeenCalledTimes(1)
421+
expect(consentCreateMock).toHaveBeenCalledTimes(1)
416422
})
417423

418424
expect(getOrCreateDataSubjectId).toHaveBeenCalledTimes(1)
419425

420-
const firstFetchCall = fetchSpy.mock.calls.at(0)
421-
if (!firstFetchCall) {
422-
throw new TestError('Expected consent logging fetch to be called once')
426+
const firstConsentCall = consentCreateMock.mock.calls.at(0)
427+
if (!firstConsentCall) {
428+
throw new TestError('Expected consent logging action to be called once')
423429
}
424-
const [, options] = firstFetchCall
425-
const payload = JSON.parse(options?.body as string)
430+
const [payload] = firstConsentCall
426431
expect(payload.DataSubjectId).toBe(regeneratedId)
427432

428433
expect($consent.get().DataSubjectId).toBe(regeneratedId)
429434
})
430435

431436
it('queues consent logging when offline and retries after reconnecting', async () => {
432-
const fetchSpy = vi.fn().mockResolvedValue({ ok: true })
433-
vi.stubGlobal('fetch', fetchSpy)
437+
consentCreateMock.mockResolvedValue({ data: { success: true, record: { id: 'consent-1' } } })
434438

435439
const onlineGetter = vi.spyOn(window.navigator, 'onLine', 'get')
436440
onlineGetter.mockReturnValue(false)
@@ -463,13 +467,13 @@ describe('Consent side effects', () => {
463467

464468
await consentListener?.(newState, oldState)
465469

466-
expect(fetchSpy).not.toHaveBeenCalled()
470+
expect(consentCreateMock).not.toHaveBeenCalled()
467471

468472
onlineGetter.mockReturnValue(true)
469473
window.dispatchEvent(new Event('online'))
470474

471475
await vi.waitFor(() => {
472-
expect(fetchSpy).toHaveBeenCalledTimes(1)
476+
expect(consentCreateMock).toHaveBeenCalledTimes(1)
473477
})
474478

475479
onlineGetter.mockRestore()
@@ -478,8 +482,7 @@ describe('Consent side effects', () => {
478482
it('coalesces burst consent updates into the latest payload before sending', async () => {
479483
vi.useFakeTimers()
480484

481-
const fetchSpy = vi.fn().mockResolvedValue({ ok: true })
482-
vi.stubGlobal('fetch', fetchSpy)
485+
consentCreateMock.mockResolvedValue({ data: { success: true, record: { id: 'consent-1' } } })
483486

484487
let consentListener:
485488
| ((_state: ConsentState, _oldState?: ConsentState) => Promise<void> | void)
@@ -524,38 +527,34 @@ describe('Consent side effects', () => {
524527
await consentListener?.(state2, state1)
525528
await consentListener?.(state3, state2)
526529

527-
expect(fetchSpy).not.toHaveBeenCalled()
530+
expect(consentCreateMock).not.toHaveBeenCalled()
528531

529532
await vi.advanceTimersByTimeAsync(250)
530533

531534
await vi.waitFor(() => {
532-
expect(fetchSpy).toHaveBeenCalledTimes(1)
535+
expect(consentCreateMock).toHaveBeenCalledTimes(1)
533536
})
534537

535-
const firstFetchCall = fetchSpy.mock.calls.at(0)
536-
if (!firstFetchCall) {
537-
throw new TestError('Expected coalesced consent logging fetch to be called once')
538+
const firstConsentCall = consentCreateMock.mock.calls.at(0)
539+
if (!firstConsentCall) {
540+
throw new TestError('Expected coalesced consent logging action to be called once')
538541
}
539542

540-
const [, options] = firstFetchCall
541-
const payload = JSON.parse(options?.body as string)
543+
const [payload] = firstConsentCall
542544
expect(payload.purposes).toEqual(['analytics', 'marketing', 'functional'])
543545
})
544546

545547
it('retries consent logging after a 429 without reporting a script error', async () => {
546548
vi.useFakeTimers()
547549

548-
const fetchSpy = vi
549-
.fn()
550+
consentCreateMock
550551
.mockResolvedValueOnce({
551-
ok: false,
552-
status: 429,
553-
statusText: 'Too Many Requests',
554-
headers: { get: vi.fn(() => '1') },
555-
json: vi.fn().mockResolvedValue({ error: { message: 'Try again in 1s' } }),
552+
error: {
553+
code: 'TOO_MANY_REQUESTS',
554+
message: 'Try again in 1s',
555+
},
556556
})
557-
.mockResolvedValueOnce({ ok: true })
558-
vi.stubGlobal('fetch', fetchSpy)
557+
.mockResolvedValueOnce({ data: { success: true, record: { id: 'consent-1' } } })
559558

560559
const handleScriptErrorSpy = vi.spyOn(errorHandlerModule, 'handleScriptError')
561560

@@ -590,15 +589,15 @@ describe('Consent side effects', () => {
590589
await vi.advanceTimersByTimeAsync(250)
591590

592591
await vi.waitFor(() => {
593-
expect(fetchSpy).toHaveBeenCalledTimes(1)
592+
expect(consentCreateMock).toHaveBeenCalledTimes(1)
594593
})
595594

596595
expect(handleScriptErrorSpy).not.toHaveBeenCalled()
597596

598597
await vi.advanceTimersByTimeAsync(1_000)
599598

600599
await vi.waitFor(() => {
601-
expect(fetchSpy).toHaveBeenCalledTimes(2)
600+
expect(consentCreateMock).toHaveBeenCalledTimes(2)
602601
})
603602

604603
expect(handleScriptErrorSpy).not.toHaveBeenCalled()

‎src/components/scripts/store/consent.ts‎

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* Cookie Consent State Management
33
*/
4+
import { actions } from 'astro:actions'
45
import { computed, onMount } from 'nanostores'
56
import { persistentAtom } from '@nanostores/persistent'
67
import { StoreController } from '@nanostores/lit'
@@ -413,8 +414,8 @@ export function initConsentSideEffects(): void {
413414
}, delayMs)
414415
}
415416

416-
const parseRetryAfterMs = (response: Response, serverMessage?: string): number => {
417-
const retryAfterHeader = response.headers.get('Retry-After')
417+
const parseRetryAfterMs = (response?: Response, serverMessage?: string): number => {
418+
const retryAfterHeader = response?.headers.get('Retry-After')
418419
if (retryAfterHeader) {
419420
const retryAfterSeconds = Number(retryAfterHeader)
420421
if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
@@ -507,45 +508,46 @@ export function initConsentSideEffects(): void {
507508
}
508509

509510
const sendConsentPayload = async (payload: ConsentLogPayload) => {
510-
const response = await fetch('/_actions/gdpr.consentCreate', {
511-
method: 'POST',
512-
headers: { 'Content-Type': 'application/json' },
513-
body: JSON.stringify(payload),
514-
})
511+
const { data, error } = await actions.gdpr.consentCreate(payload)
515512

516-
if (!response.ok) {
517-
const responseBody = await response.json().catch(() => null)
518-
const serverError =
519-
responseBody && typeof responseBody === 'object' && 'error' in responseBody
520-
? (responseBody as { error: { message?: string } }).error
521-
: null
522-
const serverMessage =
523-
serverError?.message ??
524-
(responseBody && typeof (responseBody as { message?: string }).message === 'string'
525-
? (responseBody as { message: string }).message
526-
: undefined)
527-
528-
if (response.status === 429) {
529-
throw new ConsentLogRetryableError(
530-
serverMessage ?? 'Consent logging is temporarily rate limited',
531-
parseRetryAfterMs(response, serverMessage),
532-
{
533-
status: response.status,
534-
statusText: response.statusText,
535-
body: responseBody,
536-
}
537-
)
538-
}
513+
if (!error && data?.success) {
514+
return
515+
}
539516

540-
throw new ClientScriptError({
541-
message: serverMessage ?? `Failed to record consent (status ${response.status})`,
542-
cause: {
543-
status: response.status,
544-
statusText: response.statusText,
545-
body: responseBody,
546-
},
547-
})
517+
const actionError = error as
518+
| {
519+
code?: string
520+
message?: string
521+
status?: number
522+
statusText?: string
523+
}
524+
| undefined
525+
526+
const serverMessage =
527+
typeof actionError?.message === 'string' && actionError.message.trim().length > 0
528+
? actionError.message
529+
: undefined
530+
531+
if (actionError?.code === 'TOO_MANY_REQUESTS') {
532+
throw new ConsentLogRetryableError(
533+
serverMessage ?? 'Consent logging is temporarily rate limited',
534+
parseRetryAfterMs(undefined, serverMessage),
535+
{
536+
code: actionError.code,
537+
status: actionError.status,
538+
statusText: actionError.statusText,
539+
}
540+
)
548541
}
542+
543+
throw new ClientScriptError({
544+
message: serverMessage ?? 'Failed to record consent',
545+
cause: {
546+
code: actionError?.code,
547+
status: actionError?.status,
548+
statusText: actionError?.statusText,
549+
},
550+
})
549551
}
550552

551553
$consent.subscribe((consentState, oldConsentState) => {

0 commit comments

Comments
 (0)