Skip to content

Commit 4e6fe44

Browse files
committed
Merge branch 'release/26.19.0'
2 parents 35e4ba7 + 39a4e67 commit 4e6fe44

38 files changed

Lines changed: 750 additions & 243 deletions

File tree

‎CHANGELOG‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
# Changelog
22

33
We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.
4+
5+
26.19.0 (2026-09-23)
6+
====================
7+
* Remove leave this view from top of registration VOLs
8+
* OSF links do not include any content previews when shared
9+
* Registrations resources page UI is limited to only ten items
10+
* Registration without license returns 'Not Found'
11+
* Project Overview page displays subjects and tags twice
12+
* Social settings page displays translation keys instead of localized text
13+
* Show Homepage Banner to Logged-Out Users
14+
* Terms of Use Shown on View-Only Links
15+
* Filter Sentry client-side errors in Angular-OSF
16+
* Banner Incorrectly Displaying on Registries Discover Page
17+
418
26.18.1 (2026-09-14)
519
====================
620

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ take up to 60 seconds once the docker build finishes.
2828
- [i18n](docs/i18n.md).
2929
- [NGXS Conventions](docs/ngxs.md).
3030
- [Testing Strategy](docs/testing.md).
31+
- [Sentry error filtering](docs/sentry.md).
3132

3233
### Optional
3334

‎docs/sentry.md‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Sentry error filtering
2+
3+
Sentry collects JavaScript errors from the OSF Angular app in the browser. Many of those events are not application bugs: flaky networks, cancelled requests, missing/deleted API resources, browser extensions, and stale tabs after a deploy.
4+
5+
Filtering happens on the client when Sentry starts. Change the lists in `src/app/core/helpers/sentry-filter.helper.ts`. That file is passed into `Sentry.init` from `src/app/core/provider/application.initialization.provider.ts`.
6+
7+
[Sentry filtering docs](https://docs.sentry.io/platforms/javascript/configuration/filtering/)
8+
9+
## How to read Sentry after this
10+
11+
If an issue disappears from Sentry, it was probably filtered here. It does not mean the user stopped hitting the error.
12+
13+
Server failures (HTTP 500–599) and real JavaScript exceptions are still sent.
14+
15+
## What we drop
16+
17+
Three independent checks. An event is dropped if **any** of them match.
18+
19+
### 1. Error message (`ignoreErrors`)
20+
21+
Sentry treats each string as a **substring**. `Failed to fetch` also matches `Failed to fetch dynamically imported module`.
22+
23+
| You will stop seeing | Typical cause |
24+
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
25+
| Handled unknown error | Sentry could not extract a real Error from Angular |
26+
| Non-Error promise rejection captured… | A promise rejected with `undefined` / `null` / a plain object |
27+
| no elements in sequence | RxJS `EmptyError` (empty observable used with `first()` / `single()`) |
28+
| ResizeObserver loop… | Browser layout warning |
29+
| Failed to fetch | Chrome/Edge: offline, CORS, blocked request, including `api.osf.io` / `addons.osf.io` |
30+
| Load failed | Safari equivalent of failed fetch (including `files.osf.io`) |
31+
| NetworkError when attempting to fetch resource | Firefox equivalent of failed fetch |
32+
| Failed to fetch dynamically imported module | Chrome/Edge: lazy chunk failed to load (often an old tab after deploy) |
33+
| error loading dynamically imported module | Firefox: same lazy-chunk failure |
34+
| Importing a module script failed | Safari: same lazy-chunk failure |
35+
| ChunkLoadError / Loading chunk … failed | Webpack/Vite chunk load failure after deploy |
36+
| AbortError / The operation was aborted / The user aborted a request | Request cancelled (navigation, timeout, user abort) |
37+
| Beacon is not defined | Extension or third-party script; not OSF (`navigator.sendBeacon` is a different API) |
38+
39+
### 2. Script URL (`denyUrls`)
40+
41+
Errors whose stack frames come from a **browser extension**, not from OSF code:
42+
43+
- `extensions/`
44+
- `chrome://`
45+
- `chrome-extension://`
46+
- `moz-extension://`
47+
- `safari-extension://`
48+
- `safari-web-extension://`
49+
- `ms-browser-extension://`
50+
51+
### 3. HTTP status below 500 (`beforeSend`)
52+
53+
If the event is an HTTP response and the status is **0–499**, it is dropped. Status is read from:
54+
55+
- Angular `HttpErrorResponse` (including nested `ngOriginalError` / `rejection` / `cause`)
56+
- `Http failure response for …: 410`
57+
- `Server returned code 404 with body "…"`
58+
- `Object captured as exception with keys: …` or `Non-Error exception captured with keys: …` when the keys look like an HTTP response
59+
60+
| Status | Meaning | Dropped? |
61+
| ------------- | -------------------------------------- | ------------------- |
62+
| 0 | No response (offline, CORS, cancelled) | Yes |
63+
| 401, 403 | Not signed in / not allowed | Yes |
64+
| 404, 410 | Missing or deleted resource | Yes |
65+
| 409, 422, 429 | Conflict, validation, rate limit | Yes |
66+
| Other 4xx | Client/request errors | Yes |
67+
| 500–599 | Server error | **No — still sent** |
68+
69+
This includes noisy issues such as `Http failure response for https://api.osf.io/v2/users/…: 410` and `Object captured as exception with keys: error, headers, … status … url` when the status is below 500.
70+
71+
**Side effect:** a 4xx that is actually a frontend bug is also dropped (for example a request URL that contains `undefined`).
72+
73+
## What still goes to Sentry
74+
75+
- HTTP 500–599
76+
- TypeError / ReferenceError / other exceptions that are not in the ignore list and have no HTTP status
77+
- HTTP-looking events where a status cannot be read
78+
79+
## Changing the filters
80+
81+
1. Open `src/app/core/helpers/sentry-filter.helper.ts`.
82+
2. Add a **string** to `SENTRY_IGNORE_ERRORS` for a stable message substring, or a **RegExp** for a pattern.
83+
3. Add to `SENTRY_DENY_URLS` only for third-party script origins.
84+
4. Change `sentryBeforeSend` only if the HTTP status rule should change (for example keep 404s that contain `undefined` in the URL).
85+
86+
After a release, confirm in the Sentry project that volume dropped and that 5xx / real exceptions still appear.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "osf",
3-
"version": "26.18.1",
3+
"version": "26.19.0",
44
"scripts": {
55
"ng": "ng",
66
"analyze-bundle": "ng build --configuration=analyze-bundle && source-map-explorer dist/**/*.js --no-border-checks",

‎src/app/core/components/osf-banners/tos-consent-banner/tos-consent-banner.component.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class TosConsentBannerComponent {
5858
* if user is authenticated we check whether is accepted terms of service to hide banner or show if not
5959
* otherwise user is not authenticated we hide banner always
6060
*/
61-
return user ? user.acceptedTermsOfService : true;
61+
return user?.id ? user.acceptedTermsOfService : true;
6262
});
6363

6464
/**
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { HttpErrorResponse } from '@angular/common/http';
2+
3+
import type { ErrorEvent, EventHint } from '@sentry/angular';
4+
5+
export const SENTRY_IGNORE_ERRORS: (string | RegExp)[] = [
6+
'Handled unknown error',
7+
'Non-Error promise rejection captured',
8+
'no elements in sequence',
9+
/ResizeObserver loop/,
10+
'error loading dynamically imported module',
11+
'Importing a module script failed',
12+
'Failed to fetch',
13+
'Load failed',
14+
'NetworkError when attempting to fetch resource',
15+
'AbortError',
16+
'The operation was aborted',
17+
'The user aborted a request',
18+
'ChunkLoadError',
19+
/Loading chunk [\w.-]+ failed/,
20+
'Beacon is not defined',
21+
];
22+
23+
export const SENTRY_DENY_URLS: (string | RegExp)[] = [
24+
/extensions\//i,
25+
/^chrome:\/\//i,
26+
/^chrome-extension:\/\//i,
27+
/^moz-extension:\/\//i,
28+
/^safari-extension:\/\//i,
29+
/^safari-web-extension:\/\//i,
30+
/^ms-browser-extension:\/\//i,
31+
];
32+
33+
const MIN_REPORTED_STATUS = 500;
34+
const MAX_UNWRAP_DEPTH = 4;
35+
36+
const STATUS_MESSAGE_PATTERNS = [
37+
/Http failure response for .*: (\d{1,3})(?:\s|$)/,
38+
/Server returned code (\d{1,3})(?:\s|$)/,
39+
];
40+
41+
const CAPTURED_OBJECT_KEYS = /(?:Object captured as exception|Non-Error exception captured) with keys: (.+)/;
42+
43+
const WRAPPER_KEYS = ['ngOriginalError', 'rejection', 'cause'] as const;
44+
const HTTP_RESPONSE_KEYS = ['url', 'statusText', 'headers', 'ok'] as const;
45+
46+
function isHttpResponseLike(value: object): value is { status: number } {
47+
const hasNumericStatus = 'status' in value && typeof (value as { status: unknown }).status === 'number';
48+
49+
return hasNumericStatus && HTTP_RESPONSE_KEYS.some((key) => key in value);
50+
}
51+
52+
function describesHttpResponse(message: string | undefined): boolean {
53+
const keys = message
54+
?.match(CAPTURED_OBJECT_KEYS)?.[1]
55+
.split(',')
56+
.map((key) => key.trim());
57+
58+
if (!keys?.includes('status')) {
59+
return false;
60+
}
61+
62+
return HTTP_RESPONSE_KEYS.some((key) => keys.includes(key));
63+
}
64+
65+
function getStatusFromMessage(message: string | undefined): number | null {
66+
if (!message) {
67+
return null;
68+
}
69+
70+
for (const pattern of STATUS_MESSAGE_PATTERNS) {
71+
const match = message.match(pattern);
72+
73+
if (match) {
74+
return Number(match[1]);
75+
}
76+
}
77+
78+
return null;
79+
}
80+
81+
function getStatusFromError(error: unknown, depth = 0): number | null {
82+
if (error instanceof HttpErrorResponse) {
83+
return error.status;
84+
}
85+
86+
if (typeof error === 'string') {
87+
return getStatusFromMessage(error);
88+
}
89+
90+
if (!error || typeof error !== 'object') {
91+
return null;
92+
}
93+
94+
if (isHttpResponseLike(error)) {
95+
return error.status;
96+
}
97+
98+
if (depth >= MAX_UNWRAP_DEPTH) {
99+
return null;
100+
}
101+
102+
for (const key of WRAPPER_KEYS) {
103+
const status = getStatusFromError((error as Record<string, unknown>)[key], depth + 1);
104+
105+
if (status !== null) {
106+
return status;
107+
}
108+
}
109+
110+
return null;
111+
}
112+
113+
function getErrorMessage(error: unknown, event: ErrorEvent): string | undefined {
114+
if (typeof error === 'string') {
115+
return error;
116+
}
117+
118+
if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') {
119+
return error.message;
120+
}
121+
122+
const values = event.exception?.values;
123+
124+
return values?.[values.length - 1]?.value;
125+
}
126+
127+
function getStatusFromSerialized(event: ErrorEvent, message: string | undefined): number | null {
128+
const serialized = event.extra?.['__serialized__'];
129+
130+
if (!serialized || typeof serialized !== 'object') {
131+
return null;
132+
}
133+
134+
const status = 'status' in serialized ? serialized.status : null;
135+
136+
if (typeof status === 'number' && (isHttpResponseLike(serialized) || describesHttpResponse(message))) {
137+
return status;
138+
}
139+
140+
if ('message' in serialized && typeof serialized.message === 'string') {
141+
return getStatusFromMessage(serialized.message);
142+
}
143+
144+
return null;
145+
}
146+
147+
function resolveHttpStatus(error: unknown, event: ErrorEvent): number | null {
148+
const message = getErrorMessage(error, event);
149+
150+
return getStatusFromError(error) ?? getStatusFromSerialized(event, message) ?? getStatusFromMessage(message);
151+
}
152+
153+
export function sentryBeforeSend(event: ErrorEvent, hint: EventHint): ErrorEvent | null {
154+
const status = resolveHttpStatus(hint.originalException, event);
155+
const isReportable = status === null || status >= MIN_REPORTED_STATUS;
156+
157+
return isReportable ? event : null;
158+
}

‎src/app/core/provider/application.initialization.provider.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { isPlatformBrowser } from '@angular/common';
22
import { inject, PLATFORM_ID, provideAppInitializer } from '@angular/core';
33

4+
import { SENTRY_DENY_URLS, SENTRY_IGNORE_ERRORS, sentryBeforeSend } from '@core/helpers/sentry-filter.helper';
45
import { OSFConfigService } from '@core/services/osf-config.service';
56

67
import { ENVIRONMENT } from './environment.provider';
@@ -43,7 +44,9 @@ export function initializeApplication() {
4344
environment: environment.production ? 'production' : 'development',
4445
maxBreadcrumbs: 50,
4546
sampleRate: 1.0,
46-
integrations: [],
47+
ignoreErrors: SENTRY_IGNORE_ERRORS,
48+
denyUrls: SENTRY_DENY_URLS,
49+
beforeSend: sentryBeforeSend,
4750
});
4851
}
4952
}

‎src/app/features/home/home.component.html‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<div class="home-container flex flex-column">
2-
<section class="flex flex-column pt-7 px-3 md:mt-7 md:px-5 xl:flex-row xl:mt-4 xl:px-4">
2+
<osf-scheduled-banner class="xl:mt-8" />
3+
4+
<section class="flex flex-column mt-7 px-3 md:px-5 xl:flex-row xl:mt-4 xl:px-4">
35
<div
46
class="flex flex-column align-items-center text-center xl:justify-content-center xl:text-left xl:align-items-start"
57
>

‎src/app/features/home/home.component.spec.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MockComponents, MockProvider } from 'ng-mocks';
33
import { ComponentFixture, TestBed } from '@angular/core/testing';
44
import { ActivatedRoute, Router } from '@angular/router';
55

6+
import { ScheduledBannerComponent } from '@osf/core/components/osf-banners/scheduled-banner/scheduled-banner.component';
67
import { IconComponent } from '@osf/shared/components/icon/icon.component';
78
import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component';
89

@@ -23,7 +24,7 @@ describe('HomeComponent', () => {
2324
activatedRouteMock = ActivatedRouteMockBuilder.create().build();
2425

2526
TestBed.configureTestingModule({
26-
imports: [HomeComponent, ...MockComponents(SearchInputComponent, IconComponent)],
27+
imports: [HomeComponent, ...MockComponents(SearchInputComponent, IconComponent, ScheduledBannerComponent)],
2728
providers: [provideOSFCore(), MockProvider(Router, routerMock), MockProvider(ActivatedRoute, activatedRouteMock)],
2829
});
2930

‎src/app/features/home/home.component.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Component, inject } from '@angular/core';
88
import { FormControl } from '@angular/forms';
99
import { Router, RouterLink } from '@angular/router';
1010

11+
import { ScheduledBannerComponent } from '@osf/core/components/osf-banners/scheduled-banner/scheduled-banner.component';
1112
import { IconComponent } from '@osf/shared/components/icon/icon.component';
1213
import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component';
1314

@@ -19,6 +20,7 @@ import { INTEGRATION_ICONS, SLIDES } from './constants/data';
1920
Carousel,
2021
Button,
2122
SearchInputComponent,
23+
ScheduledBannerComponent,
2224
IconComponent,
2325
NgOptimizedImage,
2426
NgTemplateOutlet,

0 commit comments

Comments
 (0)