Repository navigation
Report Filter
A portal administrator can set conditions that every report in Insights applies for everyone, on top of any filter a reader adds. One definition can give each reader a different part of the organisation: Management chain includes the viewer, for example, shows every manager only the people who report to them.
Availability: the global report filter is present since Stable build 1849. Stable build 1846 and earlier ignore it, so rolling back to those builds restores unfiltered reports.
Current stable rule (since 1852): switching the filter off for your own view requires both Administration and See PII, just like editing it. 1850 allowed Administration alone. The stricter check does not grant See PII automatically.
⚠️ It is not a security boundary whileEnforcePortalRolesisfalse. Every signed-in user then holds both portal permissions, so anyone can change the filter or switch it off for their own view. The editor warns about this. See Portal permissions → Switching enforcement off temporarily.
- Who can set it
- Build the filter
- What readers see
- Switching it off for your own view
- When reports are refused or empty
- Performance
- Where it is stored
The filter is edited on Administration → Copilot & reporting → Report filter; the page itself is titled Global report filter. Editing it needs both portal permissions:
| Permission | Why |
|---|---|
Administration (Portal.Administration) |
The filter changes what everyone sees |
See PII (Portal.SeePII) |
Its value picker lists people, and a filter that selects one person turns every report into that person's record |
Without See PII the page isn't offered, and its API answers 403 portalPermissionRequired. An administrator without See PII cannot bypass the filter either. To assign the independent roles, see Portal permissions.
The editor is the same one readers use for the Copilot Adoption page's own user filter:
- Attributes: the Entra ID attributes the user import stores (user name, email domain, department, job title, company, office location, country or region, state or province, usage location, user type, account status, manager and management chain), plus every custom organisation type defined on Administration → User organisations.
- Operators: is and is not, and on text attributes contains and does not contain. Is not includes people with no value.
- AND / OR: conditions joined by AND must all match. OR starts another group, and matching any one group is enough. AND binds tighter than OR, as in SQL. A filter can have up to 25 conditions.
A condition's value can be fixed (Department is Sales), or it can come from the person viewing the report. Only these comparisons are offered:
| Value | Offered for | Example | What each reader sees |
|---|---|---|---|
| Viewer's own value | Every attribute except user name, manager and management chain | Department is the viewer's own value | People in their own department |
| The viewer | User name, manager and management chain | Manager is the viewer | Their direct reports |
| Management chain includes the viewer | Everyone who reports to them, at any level | ||
| User name is the viewer | Only their own figures. Readers without See PII get no reports at all under such a filter; see Readers without See PII | ||
| The viewer's manager | Manager and management chain | Manager is the viewer's manager | Everyone who shares their manager |
| Management chain includes the viewer's manager | Their manager's whole organisation |
A text search (contains, does not contain) can't use a viewer value.
The portal finds the viewer in the imported user directory by their Entra object ID, then by sign-in name. A condition that needs a value the viewer doesn't have matches nobody, whatever its operator, is not included. So a viewer with no department in Entra ID sees no one under Department is the viewer's own value. The same happens to every viewer-relative condition when the viewer isn't in the directory at all: for example, a new account the user import hasn't reached yet, or someone the import leaves out because of UserGroupsFilter. The bar on each page tells the reader why their reports are empty.
Before you save, What you would see previews the draft against your own account: how many of the people in the directory you would see, and what each condition means for you. The filter applies to administrators too, so this is what your own reports will show.
- Save applies the filter at once on the web app instance you are using, and on every other instance within a minute.
- Saves are revision-checked. If someone else saved after you opened the page, your save is refused: "Someone else changed the global filter after you opened it. Reload to see their change before saving yours. Your unsaved changes will be lost."
- To remove the filter, remove every condition and save. Reports cover everyone again.
-
Every save is logged to the web app's Application Insights as a
GlobalFilterChangedcustom event, with who saved it (ModifiedBy), the newRevisionand the savedFilter. The filter names a person only where you chose someone by name.
customEvents
| where name == "GlobalFilterChanged"
| project timestamp,
ModifiedBy = tostring(customDimensions.ModifiedBy),
Revision = toint(customDimensions.Revision),
Filter = tostring(customDimensions.Filter)
| order by timestamp descEvery Insights page shows the filter near the top, above the reader's own filter, as locked conditions labelled Set by your administrator. Beside them, a sentence reads the filter back and says how many people it covers, for example 120 of 4,000 people. Readers can't change or remove it, and any filter they add applies on top of it. A printed page says which filter it was narrowed by.
The server applies the filter to every report request, whatever the page sends, so an edited address or a direct API call gets the same narrowed data. CSV and Excel exports follow it, and the Excel files describe it.
| Page | Narrowed by the filter | Kept tenant-wide, and labelled as such |
|---|---|---|
| Overview | The data counts, which describe all the data the service holds | |
| Reports, including Office apps | Every figure | |
| Prompt insights | Every figure, including the prompt-category mix and weekly trend. Each category group still needs at least ten people | |
| Teams Explorer | People-based figures. A call belongs to its organiser, and attendee counts, attendee-hours and presence count only the attendees the filter covers | Team-level figures (collaboration and conversations), because a team isn't a person. Meeting size still counts everyone |
| Web activity | Every figure | |
| Copilot Adoption | Every figure. The licence-type choice then lists only Copilot seat licence types, because other licence types' counts can't be narrowed and would reveal tenant-wide totals | Sections the page already labels as tenant-wide. Since Stable 1859 (PR #806): under this filter those sections (use by app, resource types, the weekly trends, the agent inventory, purchased seats, the Copilot Studio capacity snapshot and Microsoft's report figures) are left out, and the page says they aren't shown. Overriding which licence types count as Copilot seats (seatLicenceTypeIds) also needs See PII |
| Copilot investment | Every figure, the People tab and the Excel report. Credits on accounts the user import didn't match to a person are left out | Spending policies and the tenant credit balance, labelled Tenant-wide. The weekly trend (Is use growing?) covers the whole tenant, so it is hidden while a filter applies |
| Copilot spend | Every person-based figure, the People tab and the Excel report | Nothing. Whole-tenant figures (spending policies, pool balances, the by-agent ledger, and accounts or Studio credits not matched to a person) are withheld in any filtered view, not shown tenant-wide |
| Licence activity | Every figure, the People tab and the Excel export | |
| Activity analysis | Every figure | |
| Agent costs | The people sections | Agent and Azure cost figures, which belong to agents rather than people |
| DLP impact | Every figure, including the optional tenant-wide DLP section |
Two further effects:
- Activity that can't be linked to a person in the imported user directory isn't counted while the filter applies.
- A reader's own user filter is narrowed too: the attributes and values it offers are counted within the administrator's filter.
- They never see sign-in names in the filter. A condition on user name, manager or management chain shows a count instead, such as 3 named people, and the viewer's own values read as "you" and "your manager".
- If the filter leaves them only 1 to 4 people, their reports are refused with the portal's usual See PII message, because a report about so few people shows individuals' activity. That includes a filter that leaves them only themselves. The bar says why. A filter that leaves them nobody isn't refused: their reports are simply empty.
- The floor counts the people the filter admits from the directory, not the people a particular report has data for. A department, country or adoption breakdown can still show a group of fewer than five, exactly as it can with no filter.
Only a reader holding both Administration and See PII sees Switch off for my view on the bar. It shows that reader reports without the global filter; everyone else still sees filtered reports. Neither role alone is enough. The server checks both, including direct API/export requests and old bypass cookies.
- It lasts for the browser session and covers every tab in that browser. The filter comes back on in a new browser session, or when you select Switch back on.
- Exports and printouts made while it is off are unfiltered, and a printout says the filter was switched off.
- Those same readers also see Edit, which opens the Report filter page.
The filter fails closed: if it can't be applied, reports are refused rather than shown unfiltered. The API answers 503 with a code, and the page shows a message:
| The page says | Code | Cause | What to do |
|---|---|---|---|
| "The filter a portal administrator set for reports couldn't be read, so this report isn't available right now. Try again shortly." | globalFilterUnavailable |
The web app couldn't read the filter from the database | Check the database on Administration → Service health, then try again |
| "The filter a portal administrator set for reports can't be read by this version of the portal, so this report isn't available. Ask a portal administrator to review it." | globalFilterInvalid |
The saved filter isn't one this build understands | An administrator with See PII saves a replacement, or saves it with no conditions. Only someone with both permissions can bypass it meanwhile |
| "The user directory that report filters are applied to couldn't be read, so this filtered report isn't available right now. Try again shortly." | filterDirectoryUnavailable |
The imported user directory couldn't be read | As for the first row |
Other things a reader or administrator may see:
| Symptom | Cause | What to do |
|---|---|---|
| Empty reports, and the bar says some conditions depend on your own details and you aren't in the user directory | Your account isn't in the imported directory yet, or UserGroupsFilter leaves it out |
Wait for the next user import. An administrator with See PII can check on Administration → Users & scope → User data lookup |
| The bar says a condition must match your own value, "which isn't recorded for you" | That attribute is empty for you in Entra ID | Set it in Entra ID. It arrives with the next user import |
| "A condition refers to an organisation type that no longer exists, so that condition matches nobody." | A custom organisation type used by the filter was deleted | Edit the filter |
| The See PII message, and the bar says the filter leaves you a few people | The filter leaves a reader without See PII fewer than five people | Expected. Widen the filter, or assign See PII if the reader should see individuals |
| Saving fails with "The database hasn't been upgraded to hold a global filter, so it couldn't be saved." | The new binaries are running against a database that hasn't been upgraded | Run the installer, or the manual script 202610011330001_PortalGlobalFilter.manual.sql. Until then the portal behaves as if no filter is set |
- With no filter set (the default), nothing changes. The directory isn't read, and every report's SQL is unchanged.
- With a filter set, each SQL statement behind a report first loads the people the filter covers into a temporary table. On a synthetic database this took about 56 ms for 2,000 people and about 2.2 seconds for 200,000 people, per statement. A filter that admits most of a very large tenant is therefore the slow case. Typical filters, such as a department or a management chain, admit far fewer people.
- The temporary table is filled with
OPENJSON, so the analytics database needs compatibility level 130 or later. The Copilot and Power Platform imports already need the same.
In one row of dbo.portal_global_filters in the analytics database, created by the 202610011330001_PortalGlobalFilter migration. Because it is in the same database as the reports it scopes, a database restore brings back the filter that applied at the time. Each web app instance caches it for up to a minute. See Database schema.
- Copilot analytics overview
- Copilot Adoption user guide
- Copilot Adoption setup (admins)
- Work Trend Index
- Copilot investment
- Copilot spend
- Agent costs
- Prompt categories
- Copilot credit history (preview)
- Agent 365 package catalog
- The Copilot data in depth
- Prerequisites
- App registrations setup
- Install with the installer
- Verify the deployment
- Security & permissions model
- Private endpoints (optional)
- Certificate authentication (optional)
- Manual installation
- Activity & usage analytics (Power BI)
- SharePoint page tracking