# Analyze Canvas Performance

> Analyze and audit a Power Apps canvas app. USE WHEN the user wants to analyze, profile, audit, review, diagnose, or improve a Canvas App or pa.yaml files. USE FOR: slow apps, long load times, delegation warnings, N+1 database calls, excessive collections, ForAll optimization, OnStart overload, Named Formulas, Concurrent execution, Explicit Column Selection, DelayOutput on text inputs, cross-screen control references, Power Automate overuse, unused variables, nested galleries, error handling, App.OnError, form validation, naming conventions, variable scope misuse, modern controls, responsive layout, accessibility. DO NOT USE WHEN the app has not been synced locally yet — sync it first.

- Skill: `microsoft/analyze-canvas-performance` (Agent Skill)
- Install (CLI): `npx skillmds@latest add microsoft/analyze-canvas-performance`
- Raw SKILL.md: https://api.skillmd.com/api/skills/microsoft/analyze-canvas-performance/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Microsoft (https://skillmd.com/u/microsoft)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/microsoft/analyze-canvas-performance

---


# Analyze Canvas App Performance & Quality

Perform a full audit of the existing Canvas App and produce a prioritized findings report covering performance, coding standards, app design, and error handling:

$ARGUMENTS

## CRITICAL: Review Guidance First

Before analyzing anything, you MUST read and internalize the technical reference document:

- `${CLAUDE_PLUGIN_ROOT}/references/TechnicalGuide.md` — Technical best practices, control selection, validation workflow, formulas, layout strategies

Read this file before planning any analysis.

## CRITICAL: Sync the Canvas App First

Before reading any YAML files, call the `sync_canvas` MCP tool to ensure a local copy of the canvas app YAML is present and up to date.

Only proceed after `sync_canvas` completes successfully.

## CRITICAL: Never Modify YAML Files

You MUST read `.pa.yaml` files for analysis purposes only. **Never write, edit, create, or delete any `.yaml` or `.pa.yaml` file** at any point during this skill execution. The YAML files are the source of truth for the app and must remain unchanged.

---

## Analysis Workflow

### 0. Ask for Reviewer Name

Before doing anything else, ask the user:

> What name should appear as the author of this report? (used in "Audited by", "Reviewed by", and "Generated by" fields)

Store the answer as `{{AUTHOR}}`. If the user provides no answer, use `"Power Apps Team"`.

---

### 1. Discover App Structure

Run the following **in parallel** immediately after `sync_canvas`:

1. Read every `.pa.yaml` file and build a structural inventory.
   - **Important:** `App.pa.yaml` is the **app-level object file**, not a screen. Never treat it as a screen or create a screen dependency entry for it. It is the source of `App.OnStart`, `App.Formulas`, `App.OnError`, and global app settings. Attribute findings from it to `App` (not a screen name).
2. Call `get_appchecker_errors` — retrieves all formula errors, performance warnings, and data-source issues detected by the Power Apps App Checker engine.
3. Call `get_accessibility_errors` — retrieves all accessibility issues detected by the Power Apps Accessibility Checker.

Store the raw results from both tools — they will be processed in **Section 6**.

From the YAML files inventory:

- Number of screens and total control count per screen
- All data sources (SharePoint, Dataverse, SQL, Collections)
- All connectors and Power Automate flow calls
- `App.OnStart`, `App.Formulas`, and `App.OnError` content
- All global variables (`Set()`), context variables (`UpdateContext()`), and collections (`ClearCollect()`/`Collect()`)

Share a discovery summary:

> **Discovery complete.**
>
> | Screen | Control Count | Data Sources Referenced |
> |--------|--------------|------------------------|
> | [Name] | [N]          | [list]                 |
>
> **Total controls:** [N] | **Total screens:** [N] | **Data sources:** [list] | **Flow calls:** [N]
> **App Checker issues:** [N] | **Accessibility issues:** [N]

---

### 2. Audit — Performance

For every issue found, record: **Severity** (🔴 High / 🟡 Medium / 🟢 Low), **Location**, **Finding**, and **Recommendation**.

#### 2.1 App.OnStart Overload & Named Formulas

- Flag heavy `ClearCollect` / `Collect` / `Set` chains in `App.OnStart` that delay startup.
- Flag `Navigate()` calls inside `App.OnStart` — replace with `App.StartScreen` (declarative, doesn't block rendering):
  ```powerfx
  // GOOD
  App.StartScreen = If(Param("AdminMode") = "1", AdminScreen, HomeScreen)
  ```
- Check whether global `Set()` variables are read-only after initialization — if so, migrate to **Named Formulas** (`App.Formulas`): they are lazy, auto-updated, and don't execute at startup.
- Flag variables initialized in `OnStart` that are only used on one screen — move them to that screen's `OnVisible`.

#### 2.2 Screen OnVisible & Navigation

- Flag screens that reload data on every `OnVisible` when the data doesn't change between visits — add `If(IsEmpty(collection), ClearCollect(...))` guards.
- Identify screens with more than **50 controls** — recommend splitting or extracting components.

#### 2.3 Delegation & Data Queries

- Flag every `First(Filter(...))` or `First(Search(...))` — replace with `LookUp(...)` (delegable, returns one record without loading the dataset).
- Flag non-delegable functions in filter predicates (`Left`, `Mid`, `Len`, `IsBlank`, `in` operator, `Search()`) applied to large external tables — recommend delegable alternatives or server-side views.
- Flag hardcoded delegation row limits (default 500) where actual record count may exceed them.

#### 2.4 Large Data Payloads & Explicit Column Selection (ECS)

- Flag `ClearCollect(col, DataSource)` calls that retrieve all columns when only a subset is needed — recommend `ShowColumns()` + `Filter()`:
  ```powerfx
  // GOOD
  ClearCollect(colAcc,
      ShowColumns(Filter(Accounts, !IsBlank('Address 1: City')), "name", "address1_city")
  )
  ```
- Check whether **Explicit Column Selection (ECS)** is enabled in app settings — flag if it's off (especially for apps created before ECS was default).
- Flag `Count(Filter(...))` patterns — replace with `CountIf(DataSource, condition)` for a single delegable call.

#### 2.5 Concurrent Loading

- Flag sequential independent `ClearCollect` / `Collect` calls in `OnStart` or `OnVisible` that each target an **external data source** (Dataverse, SharePoint, SQL, a connector, or a Power Automate flow) — these are HTTP calls that can run in parallel. Wrap them in `Concurrent(...)`:
  ```powerfx
  // GOOD — each targets a different external source
  Concurrent(
      ClearCollect(colAccounts, Accounts),
      ClearCollect(colUsers, Users),
      ClearCollect(colSettings, 'Environment Variable Definitions')
  );
  ```
- ⚠️ **Do NOT flag calls that source from a local collection** (e.g., `ClearCollect(colFiltered, Filter(colLocal, ...))`) — these are in-memory operations with no HTTP overhead; `Concurrent()` adds no benefit there.
- ⚠️ Only apply `Concurrent()` to independent calls — never when one call depends on another's output.

#### 2.6 N+1 Database Calls in Galleries

- Flag `LookUp(...)`, `Filter(...)`, connector calls, or Power Automate flow calls inside gallery item properties (Label.Text, Image.Image, Icon.Color, etc.) **only when the data source is external** — i.e., a Dataverse table, a SharePoint list, a SQL table, an OData feed, or a named flow. Each gallery row triggers a separate HTTP call (N+1 pattern).
  ```powerfx
  // BAD — LookUp against an external Dataverse table; 1 HTTP call per row
  Label.Text = LookUp(Users, UserID = ThisItem.CreatedBy).FullName
  // GOOD — traverse the Dataverse relationship; resolved server-side
  Label.Text = ThisItem.'Created By'.'Full Name'
  ```
- **Do NOT flag** `LookUp(colLocalCollection, ...)` or `Filter(colLocalCollection, ...)` — local collections live in memory and have zero HTTP cost.
- Recommend: Dataverse relationships for related-record fields; `AddColumns` to pre-join external data into a local collection before binding the gallery; Dataverse views or SQL views to flatten data server-side.
- Flag `CountRows(Filter(ExternalSource, ...))` patterns inside gallery item formulas — triggers a full table scan per row render; replace with `CountIf(ExternalSource, condition)`.

#### 2.7 Nested API Calls in Formulas

- Flag connector/API calls (e.g., `Office365Users.MyProfile()`, `MSN.GetCurrentWeather()`) nested inside `Filter()`, `LookUp()`, or `ForAll()` — the call fires once per evaluation:
  ```powerfx
  // BAD — API called every time the filter evaluates
  Filter(Product, Email = Office365Users.MyProfile().Mail)
  // GOOD — call once, store result
  Set(varUserEmail, Office365Users.MyProfile().Mail);
  Filter(Product, Email = varUserEmail)
  ```
- Recommend caching the result in a variable at `App.OnStart` or via a Named Formula.

#### 2.8 Unoptimized ForAll & Patch Usage

- Flag **nested `ForAll`** — exponential iterations; recommend flattening data first.
- Flag `Patch()` inside `ForAll()` — one server call per record. Invert the pattern for a single batched call:
  ```powerfx
  // BAD — N calls
  ForAll(colData, Patch(DataSource, { field: "value" }));
  // GOOD — 1 batched call
  Patch(DataSource, ForAll(colData, { field: "value" }));
  ```
- Flag **complex or large `ForAll` chains** (multiple nested `ForAll`, `AddColumns(ForAll(...))`, or `ForAll` with multi-step `Patch`/`Collect` logic) where the data source is Dataverse — the business logic is running client-side over HTTP. Recommend moving the logic server-side:
  - **Dataverse View**: create a filtered/joined view in Dataverse and bind the gallery directly to it — eliminates the `ForAll`/`AddColumns` entirely.
  - **Low-code Dataverse Plugin** (Power Fx plugin): encapsulate the transformation logic in a server-side plugin triggered on record events.
  - **Dataverse Plug-in** (C#): for complex multi-table joins or aggregations that cannot be expressed as a view.
  - **Custom API + Dataverse Plug-in**: expose a single custom API action that accepts parameters and returns the shaped result — the app makes **one HTTP call** and receives fully processed business data, removing all client-side `ForAll` loops.
  ```powerfx
  // INSTEAD OF complex client-side ForAll:
  // Call a Custom API that returns pre-joined data
  Set(varResult, MyCustomAPI.GetOrderSummary({ customerId: gblUserId }))
  ```

#### 2.9 SharePoint Lists — Recommend Migration to Dataverse

- Identify every data source in the app that is a **SharePoint list** connector call.
- Flag each one with a 🟡 Medium finding and recommend migrating to a **Dataverse table** for the following reasons:
  - **Performance**: Dataverse supports full server-side delegation for complex queries; SharePoint delegation support is limited (e.g., no `Search()` delegation, limited `Filter` operators).
  - **Security**: Dataverse provides row-level security (column-level security, field-level security, business unit hierarchy) that SharePoint cannot match.
  - **Relational integrity**: Dataverse supports typed relationships, lookups, and choices natively — SharePoint uses flat lists.
  - **Scalability**: Dataverse tables handle millions of rows with indexed queries; SharePoint lists degrade beyond ~5,000 items.
- In addition, flag these **SharePoint-specific performance pitfalls** as separate 🟡 Medium findings:
  - **Too many dynamic lookup columns**: Person, Group, and Calculated columns add processing time inside SharePoint before data reaches the client — recommend replacing with static text/email columns where possible.
  - **Picture or Attachment columns**: inflate response payload even when not displayed — recommend removing them from the list if images are stored externally.
  - **Very large lists** (100k+ items): all columns are returned to the client even if the app only uses a few — ensure ECS is on and use `ShowColumns()` + delegable `Filter()` to limit returned data.
  - **Partition large lists** by category, year, or time window and let the user select a segment rather than querying the full list at once.
- In the recommendation, note that a migration requires: creating the Dataverse table, migrating existing data (Power Automate or dataflow), updating all formulas to use the new table name and column schema.

#### 2.10 Environment Variables — Batch Retrieval

- Identify apps that retrieve values from **Environment Variables** (i.e., `'Environment Variable Definitions'` or `LookUp('Environment Variable Values', ...)` calls).
- Flag sequential individual lookups of multiple environment variables — each is a separate HTTP call to Dataverse:
  ```powerfx
  // BAD — 3 separate HTTP calls
  Set(varApiUrl, LookUp('Environment Variable Values', 'Environment Variable Definition'.schemaname = "cr123_ApiUrl").Value);
  Set(varTenantId, LookUp('Environment Variable Values', 'Environment Variable Definition'.schemaname = "cr123_TenantId").Value);
  Set(varFeatureFlag, LookUp('Environment Variable Values', 'Environment Variable Definition'.schemaname = "cr123_FeatureFlag").Value);
  ```
- Recommend one of these patterns:
  1. **Single `ClearCollect`** — retrieve all needed environment variable values in one query and look up from the local collection:
     ```powerfx
     // GOOD — 1 HTTP call, then local lookups
     ClearCollect(colEnvVars, 'Environment Variable Values');
     Set(varApiUrl, LookUp(colEnvVars, ...).Value);
     ```
  2. **`Concurrent()`** — if the variables must each be in their own global variable, wrap all the `LookUp` / `Set` calls in `Concurrent()`:
     ```powerfx
     // GOOD — all 3 HTTP calls fire in parallel
     Concurrent(
         Set(varApiUrl, LookUp('Environment Variable Values', ...).Value),
         Set(varTenantId, LookUp('Environment Variable Values', ...).Value),
         Set(varFeatureFlag, LookUp('Environment Variable Values', ...).Value)
     );
     ```

#### 2.11 Power Automate Calls to Populate Collections

- Flag Power Automate flows called solely to retrieve data that could be queried directly from Power Apps — each flow call adds ~600ms instantiation overhead plus network latency.
- Recommend replacing with a direct connector call unless the flow performs server-side logic unavailable in Power Fx.

#### 2.12 Excessive Collections & Variables

- Flag multiple `Set()` calls that fetch different properties from the same API in separate calls — consolidate into one call and access properties:
  ```powerfx
  // BAD — 4 API calls
  Set(varName, Office365Users.MyProfile().DisplayName);
  Set(varCity, Office365Users.MyProfile().City);
  // GOOD — 1 API call
  Set(varEmployee, Office365Users.MyProfile())
  ```
- Flag unused variables or collections (declared but never read elsewhere in the app).
- Flag `Clear(); Collect(...)` patterns — replace with the single `ClearCollect(...)` call.

#### 2.13 Text Input & DelayOutput

- Flag `TextInput` controls used as live search/filter boxes where `DelayOutput` is `false` (default) — every keystroke retriggers gallery filtering or data calls.
- Recommend setting `DelayOutput = true` to introduce a ~500ms debounce before the formula evaluates.

#### 2.14 Cross-Screen Control References

- Flag any formula on Screen A that references a control on Screen B (e.g., `Screen2.Label3.Text`) — Power Apps loads Screen B into memory prematurely, increasing initial load time.
- Recommend replacing with shared variables, collections, or navigation context parameters.

#### 2.15 Control Count & Visual Complexity

- Flag screens with more than **50 controls** — recommend components or container consolidation.
- Flag `Visible = false` controls that are always rendered (non-component) — loading an invisible control still consumes memory; use conditional component rendering instead.
- Identify repeated control groups across screens that could become reusable **components**.

#### 2.16 Images & Media

- Flag `Image` controls bound to base64 strings stored in variables or collections — recommend storing URLs instead.
- Flag large PNG/JPG files embedded in the app package when SVG would suffice for icons and illustrations.
- Flag unused media files and screens bundled in the app package — recommend deleting them and republishing.

#### 2.17 Excel Connector Limitations

- Flag any data source connected via the **Excel connector** (OneDrive/SharePoint-hosted `.xlsx` file).
- Flag as 🟡 Medium with the following context:
  - Excel is not a relational database — it handles changes like a user editing a file; high-write scenarios (>50 updates/min) degrade performance.
  - The connector is limited to **2,000 rows** regardless of table size — data beyond this limit is silently truncated.
  - Wide tables (many columns) compound latency even when ECS is enabled.
- Recommend:
  1. Use the **Excel Business Online connector** if multi-user access is required.
  2. Include only the columns the app actually needs in the Excel table.
  3. Partition data into multiple smaller tables if the row limit is a constraint.
  4. **Migrate to Dataverse or Azure SQL** for any scenario requiring >2,000 rows or high write frequency.

#### 2.18 Missing App Preload Setting

- Check whether the **Preload app for enhanced performance** setting is enabled. This is detectable if the setting can be observed; otherwise, always flag as 🟢 Low advisory.
- When preloading is off, compiled JavaScript bundles, media, and control assets are not downloaded until authentication completes — users on slow networks see a long blank screen.
- Recommend enabling it:
  1. In Power Apps → select the app → **Settings** on the command bar.
  2. Set **"Preload app for enhanced performance"** to **Yes**.
  3. For Teams-embedded apps: remove and re-add the app to the Teams tab after enabling.
- ⚠️ Note: preloading exposes compiled app assets (JS, images, app name, environment URL) via unauthenticated endpoints. Data from connectors still requires authentication. Flag this trade-off if the app embeds sensitive media directly in control properties.

#### 2.19 Data Source Bottlenecks

- When the app uses **SQL Server** (on-premises or Azure SQL) or **Dataverse**, flag patterns suggesting server-side bottlenecks as 🔴 High:
  - Formulas using the `in` operator on a SQL/Dataverse column instead of `StartsWith` — `in` causes a table scan, `StartsWith` leverages an index.
  - `Filter(DataSource, condition)` on columns that are likely unindexed (non-PK, non-FK, non-choice columns in Dataverse; text columns without a SQL index).
  - Large `Sort` / `SortByColumns` calls on unindexed columns.
- Recommend:
  - Add **SQL indexes** on columns used in `Filter`, `LookUp`, and `Sort`.
  - Replace `in` with `StartsWith` where prefix matching is acceptable.
  - Check Azure SQL tier (DTU/vCore) — keep CPU utilization ≤ 75% under load.
  - Check on-premises **data gateway** health if latency is high — consider scaling out the gateway or moving to cloud-hosted data.

#### 2.20 Client, Browser & Geographic Factors

- Flag as 🟢 Low **advisory** (not fixable from YAML, but important context for slow-app reports):
  - If the app connects to an **on-premises data gateway**, note that gateway latency is added to every data call — gateway should be co-located with the data source and the majority of users.
  - Different mobile platforms (iOS vs Android) have different concurrent HTTP request limits — large data sets load unevenly; recommend smaller payloads.
  - If users are geographically distributed, recommend using **Azure SQL** or **Dataverse** (cloud-hosted, globally distributed) rather than on-premises data sources.
  - Recommend always testing on the **actual target device and network** — not just in the browser on a developer machine.

---

### 3. Audit — Coding Standards

#### 3.1 Naming Conventions

- Flag controls still using default names (`Button1`, `Label3`, `TextInput5`) — apply the standard 3-character type prefix + camelCase:

  | Control | Prefix | Control | Prefix |
  |---------|--------|---------|--------|
  | Button | `btn` | Label | `lbl` |
  | TextInput | `txt` | Gallery | `gal` |
  | Form | `frm` | Image | `img` |
  | Container | `con` | Icon | `ico` |
  | Dropdown | `drp` | ComboBox | `cmb` |

- Flag global variables without a `gbl` prefix and context variables without a `loc` prefix.
- Flag collections without a `col` prefix.
- Flag screens without a plain-language name + "Screen" suffix (e.g., `Home Screen`, `Search Screen`).

#### 3.2 Variable Scope Misuse

- Flag global variables (`Set(...)`) used for state that only matters on one screen — replace with context variables (`UpdateContext(...)`).
- Flag context variables used for values that must persist across screens — replace with global variables.
- Flag variables that share the same name as both a context variable and a global variable (silent conflict).
- Recommend Named Formulas for any read-only computed values.

#### 3.3 Missing Code Comments

- Flag formulas that are complex (nested `If`/`Switch`, multi-step `ForAll`, custom error handling) but have **no comments** — 🟢 Low.
- Flag `App.OnStart` with more than 10 `Set()`/`Collect()` calls and no section marker comments.
- Note: Power Apps **strips all comments from the compiled package** — comments have zero impact on performance, package size, or load time.
- Recommend using both comment styles:
  ```powerfx
  // Line comment — explain the next line
  /* Block comment:
     Use for multi-line explanations or section markers */
  ```

#### 3.4 Poor Code Formatting

- Flag formulas that appear as a single long line with deeply nested functions and no line breaks — 🟢 Low.
- Recommend using the **Format Text** command in the Power Apps Studio formula bar to auto-apply indentation and line breaks.
- Consistent formatting reduces review time and makes delegation warnings easier to spot.

---

### 4. Audit — App Design & Layout

#### 4.1 Nested Galleries

- Flag any gallery control whose `Items` source contains another gallery or whose child controls modify the gallery's `Items` in `OnChange` / `OnSelect` — nested galleries cause performance issues and potential infinite loops.
- Flag `ComboBox`, `Slider`, `DatePicker`, or `Toggle` controls inside galleries — their `OnChange` fires unexpectedly when data changes.

#### 4.2 Responsive Layout & Containers

- Flag apps with **Scale to Fit** enabled — this only scales, it doesn't adapt; recommend disabling and using formula-based positioning.
- Flag controls with fixed pixel coordinates instead of formula-based sizing (`Parent.Width`, `Parent.Height`).
- Flag fixed-height galleries — recommend flexible height (`Height = Parent.Height`).
- Flag controls positioned absolutely without a parent **Container** — 🟢 Low:
  - **Groups** (legacy) have no individual properties, cannot be nested logically, and are invisible to screen readers.
  - **Container controls** have their own `Width`, `Height`, `BorderColor`, and `Fill` properties; support nesting; and create a logical structure that screen readers and accessibility tools can interpret.
  - Recommend replacing groups with containers for all layout grouping beyond purely visual convenience.

#### 4.3 Modern Controls & Components

- Flag classic controls used in new screens where a **Modern Control** (Fluent Design) equivalent exists — modern controls provide built-in theming, accessibility, and reduced custom code.
- Flag large groups of theme `Set()` variables in `App.OnStart` that could be eliminated by adopting modern controls with an app theme.
- Identify duplicate control groups (e.g., navigation bar rebuilt on every screen) — extract into a **canvas component**.

#### 4.4 Form Design

- Flag apps with separate Create, Edit, and View forms for the same entity — recommend a single form with dynamic `FormMode` switching (`FormMode.New` / `FormMode.Edit` / `FormMode.View`).
- Flag forms with no unsaved-data protection on navigation.

---

### 5. Audit — Error Handling

#### 5.1 Missing Formula-Level Error Handling

- Flag `Patch()`, `SubmitForm()`, `Remove()`, and connector calls with no `IfError(...)` wrapper and no user notification on failure.
  ```powerfx
  // GOOD
  IfError(
      Patch(Orders, Defaults(Orders), { Status: "New" }),
      Notify("Save failed: " & FirstError.Message, NotificationType.Error)
  )
  ```

#### 5.2 Missing Form OnFailure Handler

- Flag `SubmitForm(frm...)` buttons where the form's `OnFailure` property is empty — users receive no feedback when submission fails.
  Recommend: `OnFailure = Notify("Error: " & Form.Error, NotificationType.Error)`

#### 5.3 Missing App.OnError

- Flag apps where `App.OnError` is empty — unhandled errors show raw system messages to users and are not logged.
  ```powerfx
  // GOOD — centralized error logging
  Notify(
      Concatenate("Error: ", FirstError.Message,
          " | Source: ", FirstError.Source),
      NotificationType.Error
  )
  ```

#### 5.4 Observability Recommendations

- Check the **App** object for an Application Insights connection string property:
  - If **not set**: flag as 🟡 Medium — recommend connecting to Application Insights for session telemetry, TTI/TTFL trends, and error visibility (see Monitoring section of report for setup steps).
  - If **set**: flag the following two as 🟢 Low advisory improvements:
    1. **Unhandled Error Monitoring** (experimental): in Power Apps Studio → Settings → Updates → Experimental → enable **"Pass errors to Azure Application Insights"**. This logs all unhandled Power Fx runtime errors to the `traces` table with the message `"Unhandled error"`, enabling proactive alerting without waiting for user reports.
       ```kql
       traces
       | where message == "Unhandled error"
       | extend d = parse_json(customDimensions)
       | extend errors = parse_json(tostring(d.errors))
       | mv-expand errors
       | project timestamp, AppName = d['ms-appName'], ErrorMessage = errors.Message
       | order by timestamp desc
       ```
    2. **Correlation Tracing** (experimental, custom connectors only): in Settings → Updates → Experimental → enable **"Enable Azure Application Insights correlation tracing"**. Propagates W3C trace context to custom connectors, enabling end-to-end distributed tracing when the downstream service is also connected to App Insights.
       ⚠️ Works **only with custom connectors** — standard connectors are not supported.

---

---

### 6. Audit — App Checker & Accessibility

Process the raw results collected in Step 1 from `get_appchecker_errors` and `get_accessibility_errors`.

#### 6.1 App Checker Errors & Warnings

For each item returned by `get_appchecker_errors`:

- Map the built-in severity from the tool to the report scale:
  - **Error** → 🔴 High
  - **Warning** → 🟡 Medium
  - **Suggestion / Info** → 🟢 Low
- Record: **Category** = `App Checker`, **Area** = the checker rule name or category (e.g., `Formula Error`, `Performance`, `Data Source`, `Deprecated`), **Location** = screen + control path as returned by the tool, **Code Found** = the offending formula snippet (≤ 120 chars), **Finding** = the checker message verbatim, **Recommendation** = actionable fix.
- Do NOT duplicate findings already captured in sections 2–5 for the same location and formula — add a note `(also flagged in Section [N])` instead of a full duplicate row.
- If `get_appchecker_errors` returns no issues, record a single 🟢 Low informational row: *"App Checker returned no errors or warnings. The app is clean per the checker."*

#### 6.2 Accessibility Errors

For each item returned by `get_accessibility_errors`:

- Map severity:
  - **Error** → 🔴 High
  - **Warning** → 🟡 Medium
  - **Suggestion** → 🟢 Low
- Record: **Category** = `Accessibility`, **Area** = the accessibility rule (e.g., `Missing Label`, `Color Contrast`, `Tab Order`, `Screen Reader`, `Keyboard Navigation`), **Location** = screen + control name, **Code Found** = the property value that caused the issue (e.g., the empty `AccessibleLabel` value), **Finding** = the checker message verbatim, **Recommendation** = actionable fix.
- If `get_accessibility_errors` returns no issues, record a single 🟢 Low informational row: *"Accessibility Checker returned no errors. The app meets baseline accessibility standards."*

---

### 7. Query & Data Source Analysis

Analyze all data access patterns in the app. Read `.pa.yaml` files only — do not modify them.

#### 7.1 App.OnStart Query Load Time Estimation

Read the `OnStart` property from `App.pa.yaml` and identify every query against an **external data source** (Dataverse, SharePoint, SQL, connector call, Power Automate flow). Classify each query by its execution context:

- **Sequential top-level**: each counts as **100 ms**.
- **Inside `Concurrent()`**: the entire `Concurrent` block counts as **100 ms** (all queries inside fire in parallel — the block's cost is the slowest single query, approximated as 100 ms).
- **Inside an `If()` condition**: the query may or may not run at runtime. Track separately as a conditional query.
- **Inside a `ForAll()` loop**: mark as N+1. Each distinct query call inside the loop costs 100 ms per iteration. Report for the best case (loop runs exactly once).

Produce three KPI values to render in the HTML report:

| KPI | Calculation Rule |
|-----|-----------------|
| **Best-Case Start Time** | Sum 100 ms per sequential query and per `Concurrent` block that is **not** inside any `If()` branch (conditional queries assumed false — skipped). |
| **Worst-Case Start Time** | Sum 100 ms per sequential query and per `Concurrent` block **including** those inside `If()` branches (all branches assumed true). |
| **ForAll N+1 Risk (1 iteration)** | For each `ForAll` that contains external queries: sum 100 ms × number of distinct query calls inside, assuming the loop runs exactly once. Note in the report that actual runtime cost = this value × N records. |

#### 7.2 Per-Screen Data Source Operations & Duplicate Detection

For each **screen** `.pa.yaml` file (skip `App.pa.yaml` — it is not a screen):

- Enumerate every `Filter()`, `LookUp()`, `Search()`, `ClearCollect()`, `Collect()` call against an external data source, plus any connector or Power Automate flow call.
- Record: **Screen**, **Control.Property**, **Data Source**, **Operation** (Filter / LookUp / Search / ClearCollect / Collect / Connector / Flow), **Filter Condition** (exact predicate text extracted from the formula).
- **Duplicate detection**: within each screen, group entries by data source. Flag any group where two or more queries share the same data source with the same or structurally similar filter condition (same field names and operators; literal values may differ):
  - 🔴 **Identical** — same data source, same filter condition: redundant HTTP call. Severity: High.
  - 🟡 **Near-Duplicate** — same data source, filter differs only in literal values: consolidation candidate. Severity: Medium.

#### 7.3 Named Formula Query & Connector Usage Per Screen

From `App.pa.yaml` → `App.Formulas`, identify every named formula that contains:
- A query against an external data source (`Filter`, `LookUp`, `Search`, `ClearCollect`, `Collect`), or
- A connector method call (e.g., `Office365Users.*`, `MSN.*`, any custom connector method), or
- A Power Automate flow call.

For each qualifying named formula:
1. Search all screen `.pa.yaml` files for references to this formula name.
2. Count how many times it is referenced per screen.
3. Report: **Formula Name**, **Data Source / Connector / Flow Referenced**, per-screen usage count.

#### 7.4 Repeated Queries Per Screen

For each screen, identify pairs or groups of queries where:
- Both/all queries target the **same external data source**, AND
- Filter conditions are **identical** or differ only in a literal value (e.g., `Status = "Active"` vs `Status = "Inactive"`).

Flag 🔴 **High** for identical duplicate queries (pure redundant round-trips). Flag 🟡 **Medium** for near-duplicates (consolidation opportunity). Include the control.property and formula snippet for each query in the group.

#### 7.5 Wide-Column Queries

Identify every query that retrieves records without restricting columns — no `ShowColumns()` wrapper and no ECS-enforced explicit column selection — from an external data source. For each:

- Record: **location** (screen name or `App.OnStart` / `App.Formulas`), **control.property**, exact **query code** (≤ 120 chars), **data source name**, **estimated columns returned**.
- Estimate column count:
  - If the data source schema is visible in the YAML (e.g., Dataverse column metadata listed), count the columns.
  - Otherwise record: `schema not determinable from YAML — all columns returned`.
- Sort results **descending by estimated column count**.
- Severity: 🔴 **High** if estimated columns ≥ 20 · 🟡 **Medium** if 10–19 · 🟢 **Low** if < 10 (but `ShowColumns()` still recommended).

---

### 8. Generate HTML Report File

After completing all audits, determine the output filename:

1. Try `Performance & Quality Review.html` first.
2. If a file with that name already exists in the same directory as the `.pa.yaml` files, append a timestamp: `Performance & Quality Review_YYYYMMDD_HHmmss.html` (e.g. `Performance & Quality Review_20260419_143022.html`).

Write the report using the template below, replacing all `{{PLACEHOLDER}}` tokens with real data.

Then post a brief in-chat confirmation:
> **Report saved:** `[resolved filename]`
> 🔴 High: [N] · 🟡 Medium: [N] · 🟢 Low: [N] · Total findings: [N]
> Open the file in a browser, then copy-paste the content into Outlook to share.

---

#### HTML Template

For the **Code Found** column extract the exact formula or property value from the `.pa.yaml` file (≤ 120 characters). Use `<code>` tags for inline formulas. For missing/empty properties write `<em>(property empty)</em>`. For structural issues write the count, e.g. `<em>(62 controls)</em>`.

For severity badges use:
- High → `<span style="background:#C50F1F;color:#fff;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">High</span>`
- Medium → `<span style="background:#F7630C;color:#fff;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">Medium</span>`
- Low → `<span style="background:#107C10;color:#fff;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">Low</span>`

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Canvas App Audit Report — {{APP_NAME}}</title>
</head>
<body style="margin:0;padding:0;background-color:#f3f2f1;font-family:'Segoe UI',Arial,sans-serif;font-size:14px;color:#323130;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#f3f2f1;">
  <tr><td align="center" style="padding:32px 16px;">
  <table width="800" cellpadding="0" cellspacing="0" style="max-width:800px;background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.12);">

    <!-- HEADER -->
    <tr>
      <td style="background-color:#742774;padding:32px 40px;">
        <table width="100%" cellpadding="0" cellspacing="0">
          <tr>
            <td>
              <p style="margin:0 0 4px 0;font-size:11px;color:#e6c9e6;letter-spacing:1px;text-transform:uppercase;">Power Apps · Canvas App</p>
              <h1 style="margin:0 0 8px 0;font-size:26px;font-weight:700;color:#ffffff;">Performance &amp; Quality Audit</h1>
              <p style="margin:0;font-size:18px;color:#e6c9e6;font-weight:400;">{{APP_NAME}}</p>
            </td>
            <td align="right" valign="top">
              <p style="margin:0;font-size:12px;color:#d4a8d4;">{{REPORT_DATE}}</p>
              <p style="margin:4px 0 0 0;font-size:12px;color:#d4a8d4;">Audited by: {{AUTHOR}}</p>
            </td>
          </tr>
        </table>
      </td>
    </tr>

    <!-- SCORE CARDS -->
    <tr>
      <td style="padding:28px 40px 0 40px;">
        <table width="100%" cellpadding="0" cellspacing="0">
          <tr>
            <td style="padding-right:12px;">
              <table width="100%" cellpadding="16" style="background:#fde7e9;border-radius:6px;border-left:4px solid #C50F1F;">
                <tr><td>
                  <p style="margin:0;font-size:11px;color:#842029;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">High Severity</p>
                  <p style="margin:4px 0 0 0;font-size:32px;font-weight:700;color:#C50F1F;">{{TOTAL_HIGH}}</p>
                </td></tr>
              </table>
            </td>
            <td style="padding-right:12px;">
              <table width="100%" cellpadding="16" style="background:#fff4ce;border-radius:6px;border-left:4px solid #F7630C;">
                <tr><td>
                  <p style="margin:0;font-size:11px;color:#7a4100;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Medium Severity</p>
                  <p style="margin:4px 0 0 0;font-size:32px;font-weight:700;color:#F7630C;">{{TOTAL_MEDIUM}}</p>
                </td></tr>
              </table>
            </td>
            <td style="padding-right:12px;">
              <table width="100%" cellpadding="16" style="background:#dff6dd;border-radius:6px;border-left:4px solid #107C10;">
                <tr><td>
                  <p style="margin:0;font-size:11px;color:#0e4a0b;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Low Severity</p>
                  <p style="margin:4px 0 0 0;font-size:32px;font-weight:700;color:#107C10;">{{TOTAL_LOW}}</p>
                </td></tr>
              </table>
            </td>
            <td>
              <table width="100%" cellpadding="16" style="background:#ede0f5;border-radius:6px;border-left:4px solid #742774;">
                <tr><td>
                  <p style="margin:0;font-size:11px;color:#4a1a5c;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Total Findings</p>
                  <p style="margin:4px 0 0 0;font-size:32px;font-weight:700;color:#742774;">{{TOTAL_FINDINGS}}</p>
                </td></tr>
              </table>
            </td>
          </tr>
        </table>
      </td>
    </tr>

    <!-- APP OVERVIEW -->
    <tr>
      <td style="padding:28px 40px 0 40px;">
        <h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">App Overview</h2>
        <table width="100%" cellpadding="8" cellspacing="0" style="border-collapse:collapse;font-size:13px;">
          <tr style="background:#f3f2f1;">
            <td style="padding:8px 12px;font-weight:600;width:200px;">Screens</td>
            <td style="padding:8px 12px;">{{SCREEN_COUNT}}</td>
            <td style="padding:8px 12px;font-weight:600;width:200px;">Total Controls</td>
            <td style="padding:8px 12px;">{{CONTROL_COUNT}}</td>
          </tr>
          <tr>
            <td style="padding:8px 12px;font-weight:600;">Data Sources</td>
            <td style="padding:8px 12px;" colspan="3">{{DATA_SOURCES}}</td>
          </tr>
          <tr style="background:#f3f2f1;">
            <td style="padding:8px 12px;font-weight:600;">Flow Calls</td>
            <td style="padding:8px 12px;">{{FLOW_CALLS}}</td>
            <td style="padding:8px 12px;font-weight:600;">Connectors</td>
            <td style="padding:8px 12px;">{{CONNECTORS}}</td>
          </tr>
        </table>
      </td>
    </tr>

    <!-- CATEGORY SUMMARY -->
    <tr>
      <td style="padding:28px 40px 0 40px;">
        <h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">Summary by Category</h2>
        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:13px;">
          <tr style="background:#742774;color:#ffffff;">
            <th style="padding:10px 12px;text-align:left;font-weight:600;">Category</th>
            <th style="padding:10px 12px;text-align:center;font-weight:600;">🔴 High</th>
            <th style="padding:10px 12px;text-align:center;font-weight:600;">🟡 Medium</th>
            <th style="padding:10px 12px;text-align:center;font-weight:600;">🟢 Low</th>
          </tr>
          <!-- Repeat one <tr> per category. Alternate row background: #ffffff / #f3f2f1 -->
          <tr style="background:#ffffff;">
            <td style="padding:9px 12px;border-bottom:1px solid #edebe9;">Performance</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{PERF_HIGH}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{PERF_MED}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{PERF_LOW}}</td>
          </tr>
          <tr style="background:#f3f2f1;">
            <td style="padding:9px 12px;border-bottom:1px solid #edebe9;">Coding Standards</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{CODE_HIGH}}</td>
           

…(truncated)
