From 8dd84ad0c13d85985dc0f0a30ff6488b3631710a Mon Sep 17 00:00:00 2001 From: Adam Kinniburgh Date: Sat, 31 Oct 2026 16:00:39 +0000 Subject: [PATCH] Add Microsoft Copilot plugin --- .../MicrosoftCopilot/v1/configValidation.json | 31 + plugins/MicrosoftCopilot/v1/custom_types.json | 16 + .../v1/dataStreams/adoptionSummary.json | 38 + .../v1/dataStreams/adoptionTrend.json | 72 ++ .../v1/dataStreams/creditCapacity.json | 95 ++ .../v1/dataStreams/environmentCapacity.json | 64 ++ .../v1/dataStreams/environments.json | 32 + .../v1/dataStreams/licences.json | 27 + .../v1/dataStreams/scripts/adoptionSummary.js | 34 + .../v1/dataStreams/scripts/adoptionTrend.js | 28 + .../scripts/environmentCapacity.js | 12 + .../errorHandling/environmentCapacity.js | 7 + .../scripts/errorHandling/spend.js | 6 + .../v1/dataStreams/scripts/licences.js | 36 + .../v1/dataStreams/scripts/spend.js | 28 + .../v1/dataStreams/scripts/userActivity.js | 47 + .../v1/dataStreams/spend.json | 102 +++ .../v1/dataStreams/userActivity.json | 221 +++++ .../v1/dataStreams/users.json | 43 + .../v1/dataStreams/validationLicences.json | 15 + .../dataStreams/validationPowerPlatform.json | 13 + .../dataStreams/validationUsageReports.json | 13 + .../v1/defaultContent/environment.dash.json | 180 ++++ .../v1/defaultContent/manifest.json | 16 + .../v1/defaultContent/overview.dash.json | 821 ++++++++++++++++++ .../v1/defaultContent/scopes.json | 36 + .../v1/defaultContent/user.dash.json | 417 +++++++++ plugins/MicrosoftCopilot/v1/docs/README.md | 103 +++ plugins/MicrosoftCopilot/v1/icon.svg | 43 + .../v1/indexDefinitions/default.json | 42 + plugins/MicrosoftCopilot/v1/metadata.json | 57 ++ plugins/MicrosoftCopilot/v1/preRequest.js | 70 ++ plugins/MicrosoftCopilot/v1/ui.json | 36 + 33 files changed, 2801 insertions(+) create mode 100644 plugins/MicrosoftCopilot/v1/configValidation.json create mode 100644 plugins/MicrosoftCopilot/v1/custom_types.json create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/adoptionSummary.json create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/adoptionTrend.json create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/creditCapacity.json create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/environmentCapacity.json create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/environments.json create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/licences.json create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/scripts/adoptionSummary.js create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/scripts/adoptionTrend.js create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/scripts/environmentCapacity.js create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/scripts/errorHandling/environmentCapacity.js create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/scripts/errorHandling/spend.js create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/scripts/licences.js create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/scripts/spend.js create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/scripts/userActivity.js create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/spend.json create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/userActivity.json create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/users.json create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/validationLicences.json create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/validationPowerPlatform.json create mode 100644 plugins/MicrosoftCopilot/v1/dataStreams/validationUsageReports.json create mode 100644 plugins/MicrosoftCopilot/v1/defaultContent/environment.dash.json create mode 100644 plugins/MicrosoftCopilot/v1/defaultContent/manifest.json create mode 100644 plugins/MicrosoftCopilot/v1/defaultContent/overview.dash.json create mode 100644 plugins/MicrosoftCopilot/v1/defaultContent/scopes.json create mode 100644 plugins/MicrosoftCopilot/v1/defaultContent/user.dash.json create mode 100644 plugins/MicrosoftCopilot/v1/docs/README.md create mode 100644 plugins/MicrosoftCopilot/v1/icon.svg create mode 100644 plugins/MicrosoftCopilot/v1/indexDefinitions/default.json create mode 100644 plugins/MicrosoftCopilot/v1/metadata.json create mode 100644 plugins/MicrosoftCopilot/v1/preRequest.js create mode 100644 plugins/MicrosoftCopilot/v1/ui.json diff --git a/plugins/MicrosoftCopilot/v1/configValidation.json b/plugins/MicrosoftCopilot/v1/configValidation.json new file mode 100644 index 00000000..8138b5a5 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/configValidation.json @@ -0,0 +1,31 @@ +{ + "steps": [ + { + "displayName": "Licence access", + "dataStream": { + "name": "validationLicences" + }, + "required": true, + "success": "Successfully read licences.", + "error": "Cannot read licences. Check the tenant ID, client ID and secret, and grant the 'LicenseAssignment.Read.All' application permission in Microsoft Entra ID." + }, + { + "displayName": "Usage report access", + "dataStream": { + "name": "validationUsageReports" + }, + "required": false, + "success": "Successfully read Microsoft 365 Copilot usage reports.", + "error": "Cannot read Microsoft 365 Copilot usage reports. Grant the 'Reports.Read.All' application permission in Microsoft Entra ID." + }, + { + "displayName": "Copilot Studio access", + "dataStream": { + "name": "validationPowerPlatform" + }, + "required": false, + "success": "Successfully read Copilot Studio credit capacity.", + "error": "Cannot read Copilot Studio credit capacity. Register the app as a Power Platform management app, as described in the setup guide." + } + ] +} diff --git a/plugins/MicrosoftCopilot/v1/custom_types.json b/plugins/MicrosoftCopilot/v1/custom_types.json new file mode 100644 index 00000000..6c587d4f --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/custom_types.json @@ -0,0 +1,16 @@ +[ + { + "name": "User", + "sourceType": "User", + "icon": "user", + "singular": "Copilot User", + "plural": "Copilot Users" + }, + { + "name": "Environment", + "sourceType": "Environment", + "icon": "layer-group", + "singular": "Environment", + "plural": "Environments" + } +] diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/adoptionSummary.json b/plugins/MicrosoftCopilot/v1/dataStreams/adoptionSummary.json new file mode 100644 index 00000000..028161bc --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/adoptionSummary.json @@ -0,0 +1,38 @@ +{ + "name": "adoptionSummary", + "displayName": "Copilot Adoption", + "description": "Enabled and active Copilot users by app over the chosen period", + "tags": ["Adoption", "Usage"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "graph/beta/copilot/reports/getMicrosoft365CopilotUserCountSummary(period='{{period || 'D28'}}')", + "postRequestScript": "adoptionSummary.js" + }, + "matches": "none", + "timeframes": false, + "ui": [ + { + "type": "radio", + "name": "period", + "label": "Period", + "defaultValue": "D28", + "options": [ + { "value": "D7", "label": "7 days" }, + { "value": "D28", "label": "28 days" }, + { "value": "D90", "label": "90 days" }, + { "value": "D180", "label": "180 days" } + ] + } + ], + "metadata": [ + { "name": "app", "displayName": "App", "shape": "string", "role": "label" }, + { "name": "enabledUsers", "displayName": "Enabled users", "shape": "number" }, + { "name": "activeUsers", "displayName": "Active users", "shape": "number" }, + { "name": "activeRate", "displayName": "Active rate", "shape": ["percent", { "decimalPlaces": 1 }] }, + { "name": "totalPrompts", "displayName": "Total prompts", "shape": "number" }, + { "name": "averagePrompts", "displayName": "Average prompts per user", "shape": ["number", { "decimalPlaces": 1 }] }, + { "name": "periodDays", "displayName": "Period (days)", "shape": "number" }, + { "name": "reportRefreshDate", "displayName": "Report refresh date", "shape": "date" } + ] +} diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/adoptionTrend.json b/plugins/MicrosoftCopilot/v1/dataStreams/adoptionTrend.json new file mode 100644 index 00000000..b3f45aaa --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/adoptionTrend.json @@ -0,0 +1,72 @@ +{ + "name": "adoptionTrend", + "displayName": "Copilot Adoption Trend", + "description": "Daily enabled and active Copilot users by app", + "tags": [ + "Adoption", + "Usage" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "graph/beta/copilot/reports/getMicrosoft365CopilotUserCountTrend(period='{{period || 'D28'}}')", + "postRequestScript": "adoptionTrend.js" + }, + "matches": "none", + "timeframes": false, + "ui": [ + { + "type": "radio", + "name": "period", + "label": "Period", + "defaultValue": "D28", + "options": [ + { + "value": "D7", + "label": "7 days" + }, + { + "value": "D28", + "label": "28 days" + }, + { + "value": "D90", + "label": "90 days" + }, + { + "value": "D180", + "label": "180 days" + } + ] + } + ], + "metadata": [ + { + "name": "reportDate", + "displayName": "Date", + "shape": "date", + "role": "timestamp" + }, + { + "name": "app", + "displayName": "App", + "shape": "string", + "role": "label" + }, + { + "name": "enabledUsers", + "displayName": "Enabled users", + "shape": "number" + }, + { + "name": "activeUsers", + "displayName": "Active users", + "shape": "number" + }, + { + "name": "promptsSubmitted", + "displayName": "Prompts submitted", + "shape": "number" + } + ] +} diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/creditCapacity.json b/plugins/MicrosoftCopilot/v1/dataStreams/creditCapacity.json new file mode 100644 index 00000000..6190d50e --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/creditCapacity.json @@ -0,0 +1,95 @@ +{ + "name": "creditCapacity", + "displayName": "Copilot Credit Capacity", + "description": "Prepaid and pay-as-you-go Copilot Credit usage for the tenant this month", + "tags": [ + "Capacity", + "Cost" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "powerplatform/licensing/entitlements/MCSMessages", + "pathToData": "entitlement", + "getArgs": [ + { + "key": "api-version", + "value": "2024-10-01" + } + ], + "expandInnerObjects": true + }, + "matches": "none", + "timeframes": false, + "metadata": [ + { + "name": "capacity.entitled.value", + "displayName": "Prepaid credits", + "shape": "number" + }, + { + "name": "capacity.consumed.value", + "displayName": "Prepaid consumed", + "shape": "number" + }, + { + "name": "capacity.allocated.value", + "displayName": "Allocated to environments", + "shape": "number" + }, + { + "name": "capacity.availableQuantity", + "displayName": "Available", + "shape": "number" + }, + { + "name": "prepaidPercentConsumed", + "displayName": "Prepaid consumed %", + "shape": [ + "percent", + { + "decimalPlaces": 1 + } + ], + "computed": true, + "valueExpression": "{{ Number($['capacity.entitled.value']) > 0 ? (Number($['capacity.consumed.value']) / Number($['capacity.entitled.value'])) * 100 : null }}" + }, + { + "name": "capacityState", + "displayName": "Capacity state", + "shape": [ + "state", + { + "map": { + "success": [ + "WithinCapacity" + ], + "warning": [ + "CoveredOverage" + ], + "error": [ + "Overage" + ] + } + } + ], + "computed": true, + "valueExpression": "{{ $['capacity.status'] }}" + }, + { + "name": "capacity.status", + "displayName": "Capacity status", + "shape": "string" + }, + { + "name": "payGo.consumed.value", + "displayName": "Pay-as-you-go consumed", + "shape": "number" + }, + { + "name": "capacity.consumed.lastUpdatedOn", + "displayName": "Last updated", + "shape": "date" + } + ] +} diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/environmentCapacity.json b/plugins/MicrosoftCopilot/v1/dataStreams/environmentCapacity.json new file mode 100644 index 00000000..7d047a44 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/environmentCapacity.json @@ -0,0 +1,64 @@ +{ + "name": "environmentCapacity", + "displayName": "Environment Credit Allocations", + "description": "Capacity allocated to the environment, by currency", + "tags": [ + "Capacity", + "Environment" + ], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "powerplatform/licensing/environments/{{object.rawId}}/allocations", + "getArgs": [ + { + "key": "api-version", + "value": "2024-10-01" + } + ], + "postRequestScript": "environmentCapacity.js", + "errorHandling": { + "type": "script", + "script": "errorHandling/environmentCapacity.js" + } + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Environment" + ] + } + }, + "timeframes": false, + "metadata": [ + { + "name": "environmentName", + "displayName": "Environment", + "shape": "string", + "role": "label" + }, + { + "name": "environmentId", + "displayName": "Environment ID", + "shape": "string", + "visible": false + }, + { + "name": "currency", + "displayName": "Currency", + "shape": "string" + }, + { + "name": "allocated", + "displayName": "Allocated", + "shape": "number" + }, + { + "name": "currencyType", + "displayName": "Currency type", + "shape": "string", + "visible": false + } + ] +} diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/environments.json b/plugins/MicrosoftCopilot/v1/dataStreams/environments.json new file mode 100644 index 00000000..90a433fd --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/environments.json @@ -0,0 +1,32 @@ +{ + "name": "environments", + "displayName": "Environments", + "description": "Power Platform environments, where Copilot Studio agents are built", + "tags": ["Environment"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "bap/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments", + "pathToData": "value", + "expandInnerObjects": true, + "getArgs": [{ "key": "api-version", "value": "2021-04-01" }], + "paging": { + "mode": "nextUrl", + "in": { "realm": "payload", "path": "nextLink" } + } + }, + "matches": "none", + "timeframes": false, + "visibility": { "type": "hidden" }, + "metadata": [ + { "name": "name", "displayName": "Environment ID", "shape": "string", "visible": false }, + { "name": "properties.displayName", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "properties.environmentSku", "displayName": "Type", "shape": "string" }, + { "name": "location", "displayName": "Geography", "shape": "string" }, + { "name": "properties.azureRegion", "displayName": "Azure region", "shape": "string" }, + { "name": "properties.isDefault", "displayName": "Default", "shape": "boolean" }, + { "name": "properties.states.management.id", "displayName": "Status", "shape": "string" }, + { "name": "properties.linkedEnvironmentMetadata.instanceUrl", "displayName": "Dataverse URL", "shape": "url" }, + { "name": "properties.createdTime", "displayName": "Created", "shape": "date" } + ] +} diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/licences.json b/plugins/MicrosoftCopilot/v1/dataStreams/licences.json new file mode 100644 index 00000000..7a7c2a2e --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/licences.json @@ -0,0 +1,27 @@ +{ + "name": "licences", + "displayName": "Copilot Licences", + "description": "Copilot licences purchased and assigned in the tenant", + "tags": ["Licence"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "graph/v1.0/subscribedSkus", + "postRequestScript": "licences.js" + }, + "matches": "none", + "timeframes": false, + "metadata": [ + { "name": "skuPartNumber", "displayName": "SKU", "shape": "string", "role": "label" }, + { "name": "skuId", "displayName": "SKU ID", "shape": "guid", "visible": false }, + { "name": "purchased", "displayName": "Purchased", "shape": "number" }, + { "name": "assigned", "displayName": "Assigned", "shape": "number" }, + { "name": "available", "displayName": "Available", "shape": "number" }, + { "name": "utilisation", "displayName": "Utilisation", "shape": ["percent", { "decimalPlaces": 1 }] }, + { "name": "suspended", "displayName": "Suspended", "shape": "number" }, + { "name": "warning", "displayName": "Warning", "shape": "number" }, + { "name": "capabilityStatus", "displayName": "Capability status", "shape": "string" }, + { "name": "copilotPlans", "displayName": "Copilot service plans", "shape": ["array", { "itemShape": "string" }] }, + { "name": "pattern", "pattern": ".*", "visible": false } + ] +} diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/scripts/adoptionSummary.js b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/adoptionSummary.js new file mode 100644 index 00000000..ca5b03e3 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/adoptionSummary.js @@ -0,0 +1,34 @@ +// One row per app. Prompt totals are tenant-level, so they are only set on the "Any app" row. +const APPS = [ + ["microsoftTeams", "Teams"], + ["word", "Word"], + ["excel", "Excel"], + ["powerPoint", "PowerPoint"], + ["outlook", "Outlook"], + ["oneNote", "OneNote"], + ["loop", "Loop"], + ["copilotChatWork", "Copilot Chat (work)"], + ["copilotChatWeb", "Copilot Chat (web)"], + ["copilotChat", "Copilot Chat (all)"], + ["m365App", "Microsoft 365 app"], + ["edge", "Edge"], + ["anyApp", "Any app"], +]; +const report = (data && data.value && data.value[0]) || {}; +const a = (report.adoptionByProduct && report.adoptionByProduct[0]) || {}; +const days = parseInt(String(a.reportPeriod || "").replace(/[^0-9]/g, ""), 10); +result = APPS.filter(([key]) => a[key + "EnabledUsers"] !== undefined).map(([key, name]) => { + const enabled = a[key + "EnabledUsers"]; + const active = a[key + "ActiveUsers"]; + const isAny = key === "anyApp"; + return { + app: name, + enabledUsers: enabled, + activeUsers: active, + activeRate: enabled > 0 ? (active / enabled) * 100 : null, + totalPrompts: isAny ? a.totalPromptsSubmitted : null, + averagePrompts: isAny ? a.averagePromptsSubmitted : null, + periodDays: Number.isNaN(days) ? null : days, + reportRefreshDate: report.reportRefreshDate, + }; +}); diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/scripts/adoptionTrend.js b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/adoptionTrend.js new file mode 100644 index 00000000..0e9d7b49 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/adoptionTrend.js @@ -0,0 +1,28 @@ +// One row per report date x app. Tenant-level prompts are only set on the "Any app" row. +const APPS = [ + ["microsoftTeams", "Teams"], + ["word", "Word"], + ["excel", "Excel"], + ["powerPoint", "PowerPoint"], + ["outlook", "Outlook"], + ["oneNote", "OneNote"], + ["loop", "Loop"], + ["copilotChatWork", "Copilot Chat (work)"], + ["copilotChatWeb", "Copilot Chat (web)"], + ["copilotChat", "Copilot Chat (all)"], + ["m365App", "Microsoft 365 app"], + ["edge", "Edge"], + ["anyApp", "Any app"], +]; +const days = ((data && data.value && data.value[0] && data.value[0].adoptionByDate) || []) + .slice() + .sort((a, b) => String(a.reportDate).localeCompare(String(b.reportDate))); +result = days.flatMap((day) => + APPS.filter(([key]) => day[key + "EnabledUsers"] !== undefined).map(([key, name]) => ({ + reportDate: day.reportDate, + app: name, + enabledUsers: day[key + "EnabledUsers"], + activeUsers: day[key + "ActiveUsers"], + promptsSubmitted: key === "anyApp" ? day.promptsSubmitted : null, + })), +); diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/scripts/environmentCapacity.js b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/environmentCapacity.js new file mode 100644 index 00000000..b9345695 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/environmentCapacity.js @@ -0,0 +1,12 @@ +// Rows need the environment's id and name, which sit outside the allocations array (id in the +// response envelope, name on the scoped object), so a declarative pathToData cannot carry them. +const prop = (v) => (Array.isArray(v) ? v[0] : v); +const env = (context.objects && context.objects[0]) || {}; +const LABELS = { MCSMessages: "Copilot Credits" }; +result = ((data && data.currencyAllocations) || []).map((a) => ({ + environmentName: prop(env.name), + environmentId: data.environmentId, + currency: LABELS[a.currencyType] || a.currencyType, + allocated: a.allocated, + currencyType: a.currencyType, +})); diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/scripts/errorHandling/environmentCapacity.js b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/errorHandling/environmentCapacity.js new file mode 100644 index 00000000..ca4efa02 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/errorHandling/environmentCapacity.js @@ -0,0 +1,7 @@ +// Environments without capacity licensing (such as developer and legacy environments) have no +// allocations resource and return 404 +const apiMsg = data && data.error && data.error.message; + +result = response.status === 404 + ? "This environment has no Copilot credit allocations." + : apiMsg || "Request failed with HTTP " + response.status; diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/scripts/errorHandling/spend.js b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/errorHandling/spend.js new file mode 100644 index 00000000..5ea32d68 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/errorHandling/spend.js @@ -0,0 +1,6 @@ +const scope = "{{dataSource.costScope}}"; +const apiMsg = data && data.error && data.error.message; + +result = !scope || scope.trim() === "" + ? "Set a Cost Management scope in the data source configuration to use this data stream." + : apiMsg || "Request failed with HTTP " + response.status; diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/scripts/licences.js b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/licences.js new file mode 100644 index 00000000..33349d4f --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/licences.js @@ -0,0 +1,36 @@ +// Keep only SKUs that grant Microsoft 365 Copilot itself, identified by its service plan IDs, so +// suites that merely bundle free Copilot features aren't counted as Copilot seats. The plan +// list shows every Copilot-related plan in the SKU for context. +const COPILOT_PLAN_IDS = [ + "a62f8878-de10-42f3-b68f-6149a25ceb97", + "3f30311c-6b1e-48a4-ab79-725b469da960", +]; +const skus = (data && data.value) || []; +result = skus + .map((s) => { + const servicePlans = s.servicePlans || []; + const isCopilot = servicePlans.some((p) => + COPILOT_PLAN_IDS.includes(String(p.servicePlanId).toLowerCase()), + ); + const plans = servicePlans.filter((p) => + String(p.servicePlanName || "").toUpperCase().includes("COPILOT"), + ); + return { s, plans, isCopilot }; + }) + .filter((x) => x.isCopilot) + .map(({ s, plans }) => { + const purchased = (s.prepaidUnits && s.prepaidUnits.enabled) || 0; + const assigned = s.consumedUnits || 0; + return { + skuPartNumber: s.skuPartNumber, + skuId: s.skuId, + purchased, + assigned, + available: purchased - assigned, + utilisation: purchased > 0 ? (assigned / purchased) * 100 : null, + suspended: (s.prepaidUnits && s.prepaidUnits.suspended) || 0, + warning: (s.prepaidUnits && s.prepaidUnits.warning) || 0, + capabilityStatus: s.capabilityStatus, + copilotPlans: plans.map((p) => p.servicePlanName), + }; + }); diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/scripts/spend.js b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/spend.js new file mode 100644 index 00000000..995fecd1 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/spend.js @@ -0,0 +1,28 @@ +// Cost Management returns { columns: [{name}], rows: [[...]] }. One output row per day x group. +const props = (data && data.properties) || {}; +const names = (props.columns || []).map((c) => c.name); +const idx = (n) => names.indexOf(n); +const groupBy = "{{groupBy || 'ServiceName'}}"; +const iDate = idx("UsageDate"); +const iCost = names.findIndex((n) => n === "Cost" || n === "PreTaxCost"); +const iQty = idx("UsageQuantity"); +const iCur = idx("Currency"); +const iGroup = idx(groupBy); + +const toDate = (v) => { + const s = String(v); + return /^[0-9]{8}$/.test(s) ? `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}T00:00:00Z` : s; +}; + +result = (props.rows || []).map((r) => { + const g = iGroup >= 0 && r[iGroup] != null && r[iGroup] !== "" ? String(r[iGroup]) : "(none)"; + const isResource = groupBy === "ResourceId"; + return { + usageDate: toDate(r[iDate]), + group: isResource ? g.split("/").filter(Boolean).pop() || g : g, + resourceId: isResource ? g : null, + cost: r[iCost], + quantity: r[iQty], + currency: r[iCur], + }; +}); diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/scripts/userActivity.js b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/userActivity.js new file mode 100644 index 00000000..860e46e4 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/scripts/userActivity.js @@ -0,0 +1,47 @@ +// One row per licensed user. Optional `users` object picker narrows the rows +// by userPrincipalName (index properties arrive as single-element arrays). +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const selected = (context.config && context.config.users) || []; +const upns = new Set( + selected + .map((o) => String(unwrap(o.userPrincipalName) ?? "").toLowerCase()) + .filter(Boolean), +); + +const DAY = 86400000; +const toDate = (s) => (s ? new Date(s + "T00:00:00Z") : null); +const orNull = (s) => s || null; + +const rows = (data.value ?? []).filter( + (r) => !upns.size || upns.has(String(r.userPrincipalName ?? "").toLowerCase()), +); + +result = rows.map((r) => { + // Days are counted from the report's own refresh date (it lags real time + // by a day or two), so the figure matches the data rather than today. + const refresh = toDate(r.reportRefreshDate); + const last = toDate(r.lastActivityDate); + const days = refresh && last ? Math.round((refresh - last) / DAY) : null; + // Rule: no activity ever = error; > 28 days idle = error; > 7 = warning. + const activity = + days === null || days > 28 ? "error" : days > 7 ? "warning" : "success"; + const p = (r.copilotActivityUserDetailsByPeriod ?? [])[0] ?? {}; + return { + displayName: r.displayName, + userPrincipalName: r.userPrincipalName, + lastActivityDate: orNull(r.lastActivityDate), + daysSinceLastActivity: days, + activityState: activity, + promptsSubmitted: p.promptsSubmittedAllApps ?? 0, + activeDays: p.activeUsageDaysAllApps ?? 0, + copilotChatLastActivityDate: orNull(r.copilotChatLastActivityDate), + teamsLastActivityDate: orNull(r.microsoftTeamsCopilotLastActivityDate), + wordLastActivityDate: orNull(r.wordCopilotLastActivityDate), + excelLastActivityDate: orNull(r.excelCopilotLastActivityDate), + powerPointLastActivityDate: orNull(r.powerPointCopilotLastActivityDate), + outlookLastActivityDate: orNull(r.outlookCopilotLastActivityDate), + oneNoteLastActivityDate: orNull(r.oneNoteCopilotLastActivityDate), + loopLastActivityDate: orNull(r.loopCopilotLastActivityDate), + reportRefreshDate: orNull(r.reportRefreshDate), + }; +}); diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/spend.json b/plugins/MicrosoftCopilot/v1/dataStreams/spend.json new file mode 100644 index 00000000..57cd1516 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/spend.json @@ -0,0 +1,102 @@ +{ + "name": "spend", + "displayName": "Copilot Spend", + "description": "Daily Azure Cost Management spend and usage quantity for Copilot and AI services, by service, meter or resource", + "tags": [ + "Cost" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "post", + "endpointPath": "arm{{dataSource.costScope}}/providers/Microsoft.CostManagement/query", + "getArgs": [ + { + "key": "api-version", + "value": "2023-11-01" + } + ], + "postBody": { + "type": "ActualCost", + "timeframe": "Custom", + "timePeriod": { + "from": "{{timeframe.start}}", + "to": "{{timeframe.end}}" + }, + "dataset": { + "granularity": "Daily", + "aggregation": { + "totalCost": { + "name": "Cost", + "function": "Sum" + }, + "totalQuantity": { + "name": "UsageQuantity", + "function": "Sum" + } + }, + "grouping": [ + { + "type": "Dimension", + "name": "{{groupBy || 'ServiceName'}}" + } + ], + "filter": "{{scopeOfCosts === 'all' ? undefined : { or: [ { dimensions: { name: 'ResourceType', operator: 'In', values: ['microsoft.powerplatform/accounts'] } }, { dimensions: { name: 'ServiceName', operator: 'In', values: ['Cognitive Services','Azure OpenAI Service','Azure AI Services','Copilot Studio','Microsoft Copilot Studio','Power Platform','Foundry Models','Foundry Tools'] } } ] } }}" + } + }, + "paging": { + "mode": "nextUrl", + "in": { + "realm": "payload", + "path": "properties.nextLink" + } + }, + "errorHandling": { + "type": "script", + "script": "errorHandling/spend.js" + }, + "postRequestScript": "spend.js" + }, + "matches": "none", + "timeframes": [ + "last7days", + "last30days", + "thisMonth", + "lastMonth", + "thisQuarter", + "lastQuarter" + ], + "ui": [ + { + "type": "radio", + "name": "groupBy", + "label": "Group by", + "defaultValue": "ServiceName", + "options": [ + { "value": "ServiceName", "label": "Service" }, + { "value": "MeterCategory", "label": "Meter category" }, + { "value": "Meter", "label": "Meter" }, + { "value": "ResourceGroupName", "label": "Resource group" }, + { "value": "ResourceId", "label": "Resource" }, + { "value": "ResourceType", "label": "Resource type" } + ] + }, + { + "type": "radio", + "name": "scopeOfCosts", + "label": "Scope of costs", + "defaultValue": "copilot", + "options": [ + { "value": "copilot", "label": "Copilot and AI services" }, + { "value": "all", "label": "All services" } + ] + } + ], + "metadata": [ + { "name": "usageDate", "displayName": "Date", "shape": "date", "role": "timestamp" }, + { "name": "group", "displayName": "Group", "shape": "string", "role": "label" }, + { "name": "resourceId", "displayName": "Resource ID", "shape": "string" }, + { "name": "cost", "displayName": "Cost", "shape": ["number", { "decimalPlaces": 2 }], "role": "value" }, + { "name": "quantity", "displayName": "Usage quantity", "shape": ["number", { "decimalPlaces": 2 }] }, + { "name": "currency", "displayName": "Currency", "shape": "string" } + ] +} diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/userActivity.json b/plugins/MicrosoftCopilot/v1/dataStreams/userActivity.json new file mode 100644 index 00000000..73209d8e --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/userActivity.json @@ -0,0 +1,221 @@ +{ + "name": "userActivity", + "displayName": "Copilot User Activity", + "description": "Microsoft 365 Copilot activity per user, including days since last use and prompts submitted", + "tags": [ + "Adoption", + "User" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "graph/beta/copilot/reports/getMicrosoft365CopilotUsageUserDetail(period='{{period || 'D28'}}')", + "postRequestScript": "userActivity.js", + "paging": { + "mode": "nextUrl", + "pageSize": { + "realm": "none" + }, + "in": { + "realm": "payload", + "path": "@odata.nextLink" + } + } + }, + "matches": "none", + "timeframes": false, + "ui": [ + { + "type": "radio", + "name": "period", + "label": "Period", + "defaultValue": "D28", + "options": [ + { + "value": "D7", + "label": "7 days" + }, + { + "value": "D28", + "label": "28 days" + }, + { + "value": "D90", + "label": "90 days" + }, + { + "value": "D180", + "label": "180 days" + } + ] + }, + { + "type": "objects", + "name": "users", + "label": "User (optional)", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "User" + ] + } + } + } + ], + "metadata": [ + { + "name": "displayName", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "userPrincipalName", + "displayName": "User principal name", + "shape": "string" + }, + { + "name": "activityState", + "displayName": "Activity", + "shape": [ + "state", + { + "map": { + "success": [ + "success" + ], + "warning": [ + "warning" + ], + "error": [ + "error" + ] + } + } + ] + }, + { + "name": "lastActivityDate", + "displayName": "Last activity", + "shape": [ + "date", + { + "format": "dd/MM/yyyy" + } + ] + }, + { + "name": "daysSinceLastActivity", + "displayName": "Days since last activity", + "shape": "number" + }, + { + "name": "lastUsed", + "displayName": "Last used", + "shape": "string", + "computed": true, + "valueExpression": "{{ $['lastActivityDate'] ? 'Last used ' + String($['lastActivityDate']).slice(0, 10) : 'Never used' }}" + }, + { + "name": "promptsSubmitted", + "displayName": "Prompts submitted", + "shape": "number" + }, + { + "name": "activeDays", + "displayName": "Active days", + "shape": "number" + }, + { + "name": "copilotChatLastActivityDate", + "displayName": "Copilot Chat last activity", + "shape": [ + "date", + { + "format": "dd/MM/yyyy" + } + ] + }, + { + "name": "teamsLastActivityDate", + "displayName": "Teams last activity", + "shape": [ + "date", + { + "format": "dd/MM/yyyy" + } + ] + }, + { + "name": "wordLastActivityDate", + "displayName": "Word last activity", + "shape": [ + "date", + { + "format": "dd/MM/yyyy" + } + ] + }, + { + "name": "excelLastActivityDate", + "displayName": "Excel last activity", + "shape": [ + "date", + { + "format": "dd/MM/yyyy" + } + ] + }, + { + "name": "powerPointLastActivityDate", + "displayName": "PowerPoint last activity", + "shape": [ + "date", + { + "format": "dd/MM/yyyy" + } + ] + }, + { + "name": "outlookLastActivityDate", + "displayName": "Outlook last activity", + "shape": [ + "date", + { + "format": "dd/MM/yyyy" + } + ] + }, + { + "name": "oneNoteLastActivityDate", + "displayName": "OneNote last activity", + "shape": [ + "date", + { + "format": "dd/MM/yyyy" + } + ] + }, + { + "name": "loopLastActivityDate", + "displayName": "Loop last activity", + "shape": [ + "date", + { + "format": "dd/MM/yyyy" + } + ] + }, + { + "name": "reportRefreshDate", + "displayName": "Report refreshed", + "shape": [ + "date", + { + "format": "dd/MM/yyyy" + } + ] + } + ] +} diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/users.json b/plugins/MicrosoftCopilot/v1/dataStreams/users.json new file mode 100644 index 00000000..7b3384f4 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/users.json @@ -0,0 +1,43 @@ +{ + "name": "users", + "displayName": "Copilot Users", + "description": "Users with a Microsoft 365 Copilot licence", + "tags": ["User", "Licence"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "graph/v1.0/users", + "pathToData": "value", + "headers": [{ "key": "ConsistencyLevel", "value": "eventual" }], + "getArgs": [ + { + "key": "$filter", + "value": "assignedPlans/any(p:p/servicePlanId eq a62f8878-de10-42f3-b68f-6149a25ceb97 and p/capabilityStatus eq 'Enabled')" + }, + { "key": "$count", "value": "true" }, + { + "key": "$select", + "value": "id,displayName,userPrincipalName,mail,department,jobTitle,officeLocation,country,accountEnabled" + } + ], + "paging": { + "mode": "nextUrl", + "pageSize": { "realm": "queryArg", "path": "$top", "value": "999" }, + "in": { "realm": "payload", "path": "@odata.nextLink" } + } + }, + "matches": "none", + "timeframes": false, + "visibility": { "type": "hidden" }, + "metadata": [ + { "name": "id", "displayName": "Object ID", "shape": "guid", "visible": false }, + { "name": "displayName", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "userPrincipalName", "displayName": "User principal name", "shape": "string" }, + { "name": "mail", "displayName": "Email", "shape": "string" }, + { "name": "department", "displayName": "Department", "shape": "string" }, + { "name": "jobTitle", "displayName": "Job title", "shape": "string" }, + { "name": "officeLocation", "displayName": "Office", "shape": "string" }, + { "name": "country", "displayName": "Country", "shape": "string" }, + { "name": "accountEnabled", "displayName": "Account enabled", "shape": "boolean" } + ] +} diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/validationLicences.json b/plugins/MicrosoftCopilot/v1/dataStreams/validationLicences.json new file mode 100644 index 00000000..bd9a24f7 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/validationLicences.json @@ -0,0 +1,15 @@ +{ + "name": "validationLicences", + "displayName": "Validation - Licences", + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "graph/v1.0/subscribedSkus", + "pathToData": "value", + "getArgs": [{ "key": "$select", "value": "skuId,skuPartNumber" }] + }, + "matches": "none", + "timeframes": false, + "providesPluginDiagnostics": true, + "visibility": { "type": "hidden" } +} diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/validationPowerPlatform.json b/plugins/MicrosoftCopilot/v1/dataStreams/validationPowerPlatform.json new file mode 100644 index 00000000..ce569474 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/validationPowerPlatform.json @@ -0,0 +1,13 @@ +{ + "name": "validationPowerPlatform", + "displayName": "Validation - Power Platform", + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "powerplatform/licensing/entitlements/MCSMessages", + "getArgs": [{ "key": "api-version", "value": "2024-10-01" }] + }, + "matches": "none", + "timeframes": false, + "visibility": { "type": "hidden" } +} diff --git a/plugins/MicrosoftCopilot/v1/dataStreams/validationUsageReports.json b/plugins/MicrosoftCopilot/v1/dataStreams/validationUsageReports.json new file mode 100644 index 00000000..481be531 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/dataStreams/validationUsageReports.json @@ -0,0 +1,13 @@ +{ + "name": "validationUsageReports", + "displayName": "Validation - Usage Reports", + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "graph/beta/copilot/reports/getMicrosoft365CopilotUserCountSummary(period='D7')", + "pathToData": "value" + }, + "matches": "none", + "timeframes": false, + "visibility": { "type": "hidden" } +} diff --git a/plugins/MicrosoftCopilot/v1/defaultContent/environment.dash.json b/plugins/MicrosoftCopilot/v1/defaultContent/environment.dash.json new file mode 100644 index 00000000..1636994e --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/defaultContent/environment.dash.json @@ -0,0 +1,180 @@ +{ + "name": "Environment", + "schemaVersion": "1.5", + "timeframe": "none", + "variables": [ + "{{variables.[Copilot Environment]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "fa354534-6766-4c88-9b70-29735ff5596f", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [ + "{{configId}}" + ], + "title": "Environment Details", + "description": "The Power Platform environment: type, region, state and Dataverse URL", + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Copilot Environments]}}", + "variable": "{{variables.[Copilot Environment]}}", + "workspace": "{{workspaceId}}" + }, + "variables": [ + "{{variables.[Copilot Environment]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "environmentType", + "title": "Type", + "renderer": { + "type": "badge", + "colors": { + "Production": "#6366f1", + "Sandbox": "#14b8a6", + "Developer": "#8b5cf6", + "Trial": "#d97706", + "Default": "#2563eb" + }, + "defaultColor": "#64748b" + } + }, + { + "field": "isDefault", + "title": "Default environment", + "renderer": { + "type": "boolean", + "trueLabel": "Yes", + "falseLabel": "No", + "trueColor": "#2563eb", + "falseColor": "#64748b" + } + }, + { + "field": "status", + "title": "State", + "renderer": { + "type": "badge", + "colors": { + "Ready": "#16a34a", + "NotSpecified": "#64748b" + }, + "defaultColor": "#d97706" + } + }, + { + "field": "geography", + "title": "Geography", + "renderer": { + "type": "text" + } + }, + { + "field": "azureRegion", + "title": "Azure region", + "renderer": { + "type": "text" + } + }, + { + "field": "createdTime", + "title": "Created", + "renderer": { + "type": "date" + } + }, + { + "field": "dataverseUrl", + "title": "Dataverse", + "renderer": { + "type": "link" + } + } + ] + }, + "style": { + "richTableCellPadding": 6, + "richTableTranspose": true + } + } + } + } + }, + { + "i": "2cef0e01-2415-4927-9967-a27b5863241f", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [ + "{{configId}}" + ], + "title": "Credit Allocations", + "description": "Capacity allocated to this environment from the tenant, by currency. Developer and some legacy environments cannot hold Copilot Credit allocations", + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[environmentCapacity]}}", + "name": "environmentCapacity", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Copilot Environments]}}", + "variable": "{{variables.[Copilot Environment]}}", + "workspace": "{{workspaceId}}" + }, + "variables": [ + "{{variables.[Copilot Environment]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "allocated", + "type": "quantitative", + "title": "Allocated" + } + ], + "label": { + "field": "currency", + "type": "nominal", + "title": "Currency" + } + }, + "style": {} + } + } + } + } + ] + } +} diff --git a/plugins/MicrosoftCopilot/v1/defaultContent/manifest.json b/plugins/MicrosoftCopilot/v1/defaultContent/manifest.json new file mode 100644 index 00000000..8bddce68 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/defaultContent/manifest.json @@ -0,0 +1,16 @@ +{ + "items": [ + { + "name": "overview", + "type": "dashboard" + }, + { + "name": "user", + "type": "dashboard" + }, + { + "name": "environment", + "type": "dashboard" + } + ] +} diff --git a/plugins/MicrosoftCopilot/v1/defaultContent/overview.dash.json b/plugins/MicrosoftCopilot/v1/defaultContent/overview.dash.json new file mode 100644 index 00000000..cdcbae9e --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/defaultContent/overview.dash.json @@ -0,0 +1,821 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "last30days", + "variables": [], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "5ed7b80b-9286-5563-bf30-0948c657b525", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [], + "title": "Seats in Use", + "description": "Microsoft 365 Copilot seats assigned out of those purchased. Amber when 40% or more of the seats are unassigned", + "timeframe": "none", + "dataStream": { + "id": "datastream-sql", + "dataSourceConfig": { + "version": "2.0", + "sql": "SELECT REPLACE(REPLACE(\"skuPartNumber\", 'Microsoft_365_', 'M365 '), '_', ' ') AS \"licence\", \"assigned\", \"purchased\" FROM \"licences\"", + "tables": [ + { + "tableName": "licences", + "config": { + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[licences]}}", + "name": "licences" + }, + "timeframe": "none" + } + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "ringGrid", + "encoding": { + "value": { + "field": "assigned", + "type": "quantitative" + }, + "max": { + "field": "purchased", + "type": "quantitative" + }, + "label": { + "field": "licence", + "type": "nominal" + } + }, + "style": { + "ringGridThresholds": [ + { + "to": 0.6, + "color": "#f59e0b" + }, + { + "to": 1, + "color": "#22c55e" + } + ], + "ringGridTrackWidth": 10, + "labelFontSize": 13 + } + } + } + } + }, + { + "i": "2b361145-82d8-518a-8f7b-5e657236ecd5", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [], + "title": "Active Users", + "description": "Copilot-enabled users who used Copilot in any app in the last 28 days", + "timeframe": "none", + "dataStream": { + "id": "datastream-sql", + "dataSourceConfig": { + "version": "2.0", + "sql": "SELECT 'Last 28 days' AS \"label\", \"activeUsers\", \"enabledUsers\" FROM \"adoption\" WHERE \"app\" = 'Any app'", + "tables": [ + { + "tableName": "adoption", + "config": { + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[adoptionSummary]}}", + "name": "adoptionSummary", + "dataSourceConfig": { + "period": "D28" + } + }, + "timeframe": "none" + } + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "ringGrid", + "encoding": { + "value": { + "field": "activeUsers", + "type": "quantitative" + }, + "max": { + "field": "enabledUsers", + "type": "quantitative" + }, + "label": { + "field": "label", + "type": "nominal" + } + }, + "style": { + "ringGridThresholds": [ + { + "to": 0.4, + "color": "#ef4444" + }, + { + "to": 0.7, + "color": "#f59e0b" + }, + { + "to": 1, + "color": "#22c55e" + } + ], + "ringGridTrackWidth": 10, + "labelFontSize": 13 + } + } + } + } + }, + { + "i": "0af47596-434f-5c25-877d-90c446b41ebc", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [], + "title": "Prompts", + "description": "Prompts submitted to Copilot across all apps in the last 28 days, and the average per active user", + "timeframe": "none", + "dataStream": { + "id": "datastream-sql", + "dataSourceConfig": { + "version": "2.0", + "sql": "SELECT \"totalPrompts\" AS \"prompts\", ROUND(\"averagePrompts\") AS \"perUser\" FROM \"adoption\" WHERE \"app\" = 'Any app'", + "tables": [ + { + "tableName": "adoption", + "config": { + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[adoptionSummary]}}", + "name": "adoptionSummary", + "dataSourceConfig": { + "period": "D28" + } + }, + "timeframe": "none" + } + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "prompts", + "type": "quantitative", + "title": "Prompts in 28 days" + }, + { + "field": "perUser", + "type": "quantitative", + "title": "Average per user" + } + ] + }, + "style": { + "scalarColumns": 1 + } + } + } + } + }, + { + "i": "d43e3456-7b1a-5b9f-a5fd-d3daf79f05ad", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [], + "title": "Copilot Studio Credits", + "description": "Whether the tenant is within its prepaid Copilot Credit capacity this month, and how many pay-as-you-go credits it has used", + "timeframe": "none", + "dataStream": { + "id": "datastream-sql", + "dataSourceConfig": { + "version": "2.0", + "sql": "SELECT CASE \"capacity.status\" WHEN 'WithinCapacity' THEN 'Within capacity' WHEN 'CoveredOverage' THEN 'Overage covered' WHEN 'Overage' THEN 'In overage' ELSE COALESCE(\"capacity.status\", 'Unknown') END AS \"label\", \"capacityState\" AS \"state\", CONCAT_WS(' · ', CASE WHEN \"capacity.entitled.value\" > 0 THEN FORMAT('{:,.0f} of {:,.0f} prepaid', \"capacity.consumed.value\", \"capacity.entitled.value\") END, FORMAT('{:,.0f} pay-as-you-go', COALESCE(\"payGo.consumed.value\", 0))) AS \"detail\" FROM \"credits\"", + "tables": [ + { + "tableName": "credits", + "config": { + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[creditCapacity]}}", + "name": "creditCapacity" + }, + "timeframe": "none" + } + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "label", + "type": "nominal" + }, + "status": { + "field": "state", + "type": "nominal" + } + }, + "style": { + "statusGridColumnsPerRow": 1, + "statusGridLabelWrap": true, + "statusGridSecondaryTemplate": "{{{{raw}}}}{{detail}}{{{{/raw}}}}", + "labelFontSize": 16, + "secondaryFontSize": 12 + } + } + } + } + }, + { + "i": "53a77d15-61db-5626-b137-22c25eb89728", + "x": 0, + "y": 2, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [], + "title": "Adoption by App", + "description": "Percentage of Copilot-enabled users who used Copilot in each app in the last 28 days", + "timeframe": "none", + "dataStream": { + "id": "datastream-sql", + "dataSourceConfig": { + "version": "2.0", + "sql": "SELECT \"app\", \"activeRate\" AS \"rate\" FROM \"adoption\" WHERE \"app\" NOT IN ('Any app', 'Copilot Chat (all)') ORDER BY \"activeRate\" DESC, \"app\"", + "tables": [ + { + "tableName": "adoption", + "config": { + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[adoptionSummary]}}", + "name": "adoptionSummary", + "dataSourceConfig": { + "period": "D28" + } + }, + "timeframe": "none" + } + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bullet", + "encoding": { + "value": { + "field": "rate", + "type": "quantitative", + "title": "Active rate" + }, + "label": { + "field": "app", + "type": "nominal" + } + }, + "style": { + "gaugeMin": 0, + "gaugeMax": 100, + "bulletFillColor": "#3b82f6", + "bulletLabelWidth": 140, + "bulletTrackHeight": 14 + } + } + } + } + }, + { + "i": "4a277e0a-e95b-596d-a930-5292561f329d", + "x": 2, + "y": 2, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [], + "title": "Daily Activity", + "description": "Users active in any Copilot app each day, and prompts submitted, over the last 28 days", + "timeframe": "none", + "dataStream": { + "id": "datastream-sql", + "dataSourceConfig": { + "version": "2.0", + "sql": "SELECT \"reportDate\" AS \"date\", \"activeUsers\", \"promptsSubmitted\" AS \"prompts\" FROM \"trend\" WHERE \"app\" = 'Any app' ORDER BY \"reportDate\"", + "tables": [ + { + "tableName": "trend", + "config": { + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[adoptionTrend]}}", + "name": "adoptionTrend", + "dataSourceConfig": { + "period": "D28" + } + }, + "timeframe": "none" + } + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "date", + "type": "temporal" + } + }, + "style": { + "cartesianAreaGradient": true, + "cartesianY2AxisField": "prompts", + "legendPosition": "bottom", + "mappedColors": { + "activeUsers": "#22c55e", + "prompts": "#3b82f6" + } + } + } + } + } + }, + { + "i": "323139b5-1cf8-56d9-b74d-da0c78a5ecee", + "x": 0, + "y": 5, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [], + "title": "Licensed Users by Activity", + "description": "Copilot-licensed users grouped by when they last used Copilot", + "timeframe": "none", + "dataStream": { + "id": "datastream-sql", + "dataSourceConfig": { + "version": "2.0", + "sql": "SELECT CASE WHEN \"lastActivityDate\" IS NULL THEN 'Never used' WHEN \"daysSinceLastActivity\" <= 7 THEN 'Active' WHEN \"daysSinceLastActivity\" <= 28 THEN 'Idle 8–28 days' ELSE 'Idle over 28 days' END AS \"activity\", COUNT(*) AS \"users\" FROM \"activity\" GROUP BY 1", + "tables": [ + { + "tableName": "activity", + "config": { + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[userActivity]}}", + "name": "userActivity", + "dataSourceConfig": { + "period": "D28" + } + }, + "timeframe": "none" + } + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "pie", + "encoding": { + "label": { + "field": "activity", + "type": "nominal" + }, + "value": { + "field": "users", + "type": "quantitative", + "aggregate": "sum" + } + }, + "style": { + "pieInnerRadius": 60, + "pieDonutCenterValue": "total", + "labelFontSize": 14, + "pieLabels": "nameAndValue", + "mappedColors": { + "Active": "#22c55e", + "Idle 8–28 days": "#f59e0b", + "Idle over 28 days": "#ef4444", + "Never used": "#6b7280" + } + } + } + } + } + }, + { + "i": "033c11f9-3f22-5ed7-8ed5-30fe5b94b6d6", + "x": 2, + "y": 5, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [], + "title": "Users Needing Attention", + "description": "Licensed users idle for more than 7 days (amber) or more than 28 days, or who have never used Copilot (red), with their prompts in the last 28 days. Candidates for training or for reclaiming the licence", + "timeframe": "none", + "dataStream": { + "id": "datastream-sql", + "dataSourceConfig": { + "version": "2.0", + "sql": "SELECT \"displayName\", \"activityState\", \"lastActivityDate\", \"daysSinceLastActivity\", \"promptsSubmitted\" FROM \"activity\" WHERE \"activityState\" <> 'success' ORDER BY \"daysSinceLastActivity\" DESC NULLS FIRST", + "tables": [ + { + "tableName": "activity", + "config": { + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[userActivity]}}", + "name": "userActivity", + "dataSourceConfig": { + "period": "D28" + } + }, + "timeframe": "none" + } + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "activityState", + "title": "", + "renderer": { + "type": "status", + "showLabel": false + }, + "width": 32 + }, + { + "field": "displayName", + "title": "User", + "renderer": { + "type": "text" + } + }, + { + "field": "lastActivityDate", + "title": "Last used", + "renderer": { + "type": "date", + "relative": true + }, + "width": 130 + }, + { + "field": "daysSinceLastActivity", + "title": "Days idle", + "renderer": { + "type": "number", + "thresholds": [ + { + "to": 7, + "color": "#22c55e" + }, + { + "to": 28, + "color": "#f59e0b" + }, + { + "to": 100000, + "color": "#ef4444" + } + ] + }, + "align": "right", + "width": 130 + }, + { + "field": "promptsSubmitted", + "title": "Prompts", + "renderer": { + "type": "number" + }, + "align": "right", + "width": 100 + } + ] + }, + "style": { + "richTableSort": { + "field": "daysSinceLastActivity", + "direction": "desc" + } + } + } + } + } + }, + { + "i": "f90a5d15-8cda-5354-b904-f9696a3ca96a", + "x": 0, + "y": 8, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [], + "title": "Top Users by Prompts", + "description": "The ten users who submitted the most Copilot prompts in the last 28 days", + "timeframe": "none", + "dataStream": { + "id": "datastream-sql", + "dataSourceConfig": { + "version": "2.0", + "sql": "SELECT \"user\", \"prompts\" FROM (SELECT \"displayName\" AS \"user\", \"promptsSubmitted\" AS \"prompts\" FROM \"activity\" WHERE \"promptsSubmitted\" > 0 ORDER BY \"promptsSubmitted\" DESC LIMIT 10) ORDER BY \"prompts\" ASC", + "tables": [ + { + "tableName": "activity", + "config": { + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[userActivity]}}", + "name": "userActivity", + "dataSourceConfig": { + "period": "D28" + } + }, + "timeframe": "none" + } + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "user", + "type": "nominal", + "title": "User" + }, + "y": { + "field": "prompts", + "type": "quantitative", + "title": "Prompts" + } + }, + "style": { + "cartesianHorizontal": true, + "cartesianBarBorderRadius": 4, + "showLabels": true, + "colors": [ + "#3b82f6" + ] + } + } + } + } + }, + { + "i": "9f452077-0ef4-57e4-a763-87cd074034ad", + "x": 2, + "y": 8, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [ + "{{configId}}" + ], + "title": "Credit Allocations by Environment", + "description": "Capacity allocated to each Power Platform environment, by currency. Environments that can't hold allocations aren't listed", + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[environmentCapacity]}}", + "name": "environmentCapacity", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "allocated", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Copilot Environments]}}", + "workspace": "{{workspaceId}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "environmentName", + "title": "Environment", + "renderer": { + "type": "text" + } + }, + { + "field": "currency", + "title": "Currency", + "renderer": { + "type": "badge", + "defaultColor": "#3b82f6" + } + }, + { + "field": "allocated", + "title": "Allocated", + "renderer": { + "type": "bar", + "color": "#3b82f6" + } + } + ] + }, + "style": {} + } + } + } + }, + { + "i": "391d9af4-cde3-556c-98ef-68b34d639d02", + "x": 0, + "y": 11, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [], + "title": "Copilot and AI Spend", + "description": "Daily Azure cost of Copilot Studio, Copilot pay-as-you-go and Azure AI services from Cost Management, by service, with each service's total for the timeframe", + "dataStream": { + "id": "datastream-sql", + "dataSourceConfig": { + "version": "2.0", + "sql": "SELECT \"usageDate\" AS \"day\", \"group\" || ' (' || \"currency\" || ')' AS \"service\", SUM(\"cost\") AS \"cost\" FROM \"spend\" GROUP BY 1, 2 ORDER BY 1", + "tables": [ + { + "tableName": "spend", + "config": { + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[spend]}}", + "name": "spend", + "dataSourceConfig": { + "groupBy": "ServiceName", + "scopeOfCosts": "copilot" + } + } + } + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "day", + "type": "temporal", + "title": "Day" + }, + "y": { + "field": "cost", + "type": "quantitative", + "title": "Cost", + "aggregate": "sum" + }, + "series": { + "field": "service", + "type": "nominal", + "title": "Service" + } + }, + "style": { + "cartesianStack": "on", + "legendPosition": "right", + "cartesianLegendStyle": "table", + "cartesianLegendStats": [ + "sum" + ], + "cartesianBarBorderRadius": 2, + "cartesianYAxisDecimalPlaces": 2 + } + } + } + } + } + ] + } +} diff --git a/plugins/MicrosoftCopilot/v1/defaultContent/scopes.json b/plugins/MicrosoftCopilot/v1/defaultContent/scopes.json new file mode 100644 index 00000000..0592fa67 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/defaultContent/scopes.json @@ -0,0 +1,36 @@ +[ + { + "name": "Copilot Users", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "User" + ] + } + }, + "variable": { + "name": "Copilot User", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Copilot Environments", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Environment" + ] + } + }, + "variable": { + "name": "Copilot Environment", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/MicrosoftCopilot/v1/defaultContent/user.dash.json b/plugins/MicrosoftCopilot/v1/defaultContent/user.dash.json new file mode 100644 index 00000000..35247362 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/defaultContent/user.dash.json @@ -0,0 +1,417 @@ +{ + "name": "Copilot User", + "schemaVersion": "1.5", + "timeframe": "none", + "variables": [ + "{{variables.[Copilot User]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "d103ff83-c267-44ea-8620-895caddcbe1d", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [ + "{{configId}}" + ], + "title": "Copilot Activity", + "description": "Active in the last 7 days (green), idle for more than 7 days (amber), or idle for more than 28 days or never used (red)", + "timeframe": "none", + "variables": [ + "{{variables.[Copilot User]}}" + ], + "dataStream": { + "id": "{{dataStreams.[userActivity]}}", + "name": "userActivity", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "period": "D28", + "users": { + "variable": "{{variables.[Copilot User]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Copilot Users]}}" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "displayName", + "type": "nominal", + "title": "Name" + }, + "status": { + "field": "activityState", + "type": "nominal", + "title": "Activity" + }, + "value": { + "field": "lastUsed", + "type": "nominal", + "title": "Last used" + } + }, + "style": { + "statusGridColumnsPerRow": 1, + "statusGridLabelWrap": true, + "labelFontSize": 16, + "secondaryFontSize": 12 + } + } + } + } + }, + { + "i": "425c8683-6f66-553d-b3a3-e22076044687", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [ + "{{configId}}" + ], + "title": "Active Days", + "description": "Days in the last 28 on which this user used Copilot in any app", + "timeframe": "none", + "variables": [ + "{{variables.[Copilot User]}}" + ], + "dataStream": { + "id": "{{dataStreams.[userActivity]}}", + "name": "userActivity", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "period": "D28", + "users": { + "variable": "{{variables.[Copilot User]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Copilot Users]}}" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "ringGrid", + "encoding": { + "value": { + "field": "activeDays", + "type": "quantitative" + } + }, + "style": { + "ringGridMax": 28, + "ringGridTrackWidth": 10, + "ringGridThresholds": [ + { + "to": 0.15, + "color": "#f59e0b" + }, + { + "to": 1, + "color": "#22c55e" + } + ] + } + } + } + } + }, + { + "i": "861a1e31-e58c-4754-94fc-535a489123ed", + "x": 2, + "y": 0, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [ + "{{configId}}" + ], + "title": "Usage (28 Days)", + "description": "Prompts this user submitted to Copilot in the last 28 days, and days since they last used it", + "timeframe": "none", + "variables": [ + "{{variables.[Copilot User]}}" + ], + "dataStream": { + "id": "{{dataStreams.[userActivity]}}", + "name": "userActivity", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "period": "D28", + "users": { + "variable": "{{variables.[Copilot User]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Copilot Users]}}" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "promptsSubmitted", + "type": "quantitative", + "title": "Prompts submitted" + }, + { + "field": "daysSinceLastActivity", + "type": "quantitative", + "title": "Days since last use" + } + ] + }, + "style": { + "scalarColumns": 2 + } + } + } + } + }, + { + "i": "eb1b900e-5c32-4cf9-8dc1-e6e26224b822", + "x": 0, + "y": 2, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [ + "{{configId}}" + ], + "title": "User Details", + "description": "The user's Entra ID profile", + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Copilot Users]}}", + "variable": "{{variables.[Copilot User]}}", + "workspace": "{{workspaceId}}" + }, + "variables": [ + "{{variables.[Copilot User]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "userPrincipalName", + "title": "User principal name", + "renderer": { + "type": "text" + } + }, + { + "field": "mail", + "title": "Email", + "renderer": { + "type": "text" + } + }, + { + "field": "jobTitle", + "title": "Job title", + "renderer": { + "type": "text" + } + }, + { + "field": "department", + "title": "Department", + "renderer": { + "type": "text" + } + }, + { + "field": "officeLocation", + "title": "Office", + "renderer": { + "type": "text" + } + }, + { + "field": "country", + "title": "Country", + "renderer": { + "type": "text" + } + }, + { + "field": "accountEnabled", + "title": "Account enabled", + "renderer": { + "type": "boolean", + "trueLabel": "Yes", + "falseLabel": "No", + "trueColor": "#16a34a", + "falseColor": "#dc2626" + } + } + ] + }, + "style": { + "richTableCellPadding": 6, + "richTableTranspose": true + } + } + } + } + }, + { + "i": "4640a3f2-063d-4353-b7bb-012ee5a2c3a7", + "x": 2, + "y": 2, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "activePluginConfigIds": [ + "{{configId}}" + ], + "title": "Last Used by App", + "description": "When this user last used Copilot in each Microsoft 365 app. Blank means never in the reporting window", + "timeframe": "none", + "variables": [ + "{{variables.[Copilot User]}}" + ], + "dataStream": { + "id": "{{dataStreams.[userActivity]}}", + "name": "userActivity", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "period": "D28", + "users": { + "variable": "{{variables.[Copilot User]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Copilot Users]}}" + } + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "copilotChatLastActivityDate", + "title": "Copilot Chat", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "teamsLastActivityDate", + "title": "Teams", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "outlookLastActivityDate", + "title": "Outlook", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "wordLastActivityDate", + "title": "Word", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "excelLastActivityDate", + "title": "Excel", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "powerPointLastActivityDate", + "title": "PowerPoint", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "oneNoteLastActivityDate", + "title": "OneNote", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "loopLastActivityDate", + "title": "Loop", + "renderer": { + "type": "date", + "relative": true + } + }, + { + "field": "reportRefreshDate", + "title": "Report refreshed", + "renderer": { + "type": "date" + } + } + ] + }, + "style": { + "richTableCellPadding": 6, + "richTableTranspose": true + } + } + } + } + } + ] + } +} diff --git a/plugins/MicrosoftCopilot/v1/docs/README.md b/plugins/MicrosoftCopilot/v1/docs/README.md new file mode 100644 index 00000000..0a146cc7 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/docs/README.md @@ -0,0 +1,103 @@ +Monitor your Microsoft Copilot estate in SquaredUp: [Microsoft 365 Copilot](https://learn.microsoft.com/en-us/copilot/microsoft-365/) adoption and licence use, [Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/) credit capacity, and Copilot spend from [Azure Cost Management](https://learn.microsoft.com/en-us/azure/cost-management-billing/). Data comes from [Microsoft Graph](https://learn.microsoft.com/en-us/graph/overview), the [Power Platform API](https://learn.microsoft.com/en-us/power-platform/admin/programmability-overview) and the [Cost Management API](https://learn.microsoft.com/en-us/rest/api/cost-management/), all through one Microsoft Entra app registration. + +> ⚠️ Copilot Studio credit consumption **per agent or per environment** isn't available to an app registration, only to a signed-in administrator, so this plugin reports Copilot Studio credits for the whole tenant. Per-agent breakdowns remain in the [Power Platform admin center](https://admin.powerplatform.microsoft.com). + +## Setup + +You need a Microsoft Entra **app registration** with a client secret, read access to Microsoft Graph reports and licences, registration as a Power Platform management app, and optionally Cost Management access. + +### 1. Register the app + +1. In the [Microsoft Entra admin center](https://entra.microsoft.com), go to **Identity → Applications → App registrations** and click **New registration**. Give it a name such as `SquaredUp Copilot` and click **Register**. +2. From the **Overview** page, copy the **Directory (tenant) ID** and **Application (client) ID** into the matching fields. +3. Go to **Certificates & secrets → Client secrets → New client secret**. Copy the secret **Value** into the **Client secret** field. + +### 2. Grant Microsoft Graph permissions + +1. In the app registration, go to **API permissions → Add a permission → Microsoft Graph → Application permissions**. +2. Add **Reports.Read.All**, **User.Read.All** and **LicenseAssignment.Read.All**. +3. Click **Grant admin consent** for your organization. + +### 3. Show user names in usage reports (optional) + +Microsoft 365 hides user names in usage reports by default. The plugin works with names hidden, but the **Copilot User Activity** data stream then shows anonymous IDs instead of names, and its rows can't be matched to imported Copilot Users, so the per-user filter and the **Copilot User** dashboard show no activity. + +This is a tenant-wide setting: showing names applies to **all** Microsoft 365 usage reports, not only Copilot's. Check your organization's privacy policy before you change it. To show names: + +1. In the [Microsoft 365 admin center](https://admin.microsoft.com), go to **Settings → Org settings → Services → Reports**. +2. Clear **Display concealed user, group, and site names in all reports** and click **Save**. + +### 4. Register the app with Power Platform + +Copilot Studio credit and environment data comes from Power Platform, which only accepts apps a Power Platform administrator has registered ([Microsoft guide](https://learn.microsoft.com/en-us/power-platform/admin/powerplatform-api-create-service-principal)). Signed in as a **Power Platform administrator** or **Global administrator**, run one of these once, replacing `TENANT_ID` with the **Directory (tenant) ID** and `CLIENT_ID` with the **Application (client) ID**. + +With the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) on any operating system: + +```bash +az login --tenant "TENANT_ID" --allow-no-subscriptions +az rest --method put --resource https://service.powerapps.com/ --url "https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/adminApplications/CLIENT_ID?api-version=2020-10-01" +``` + +Or with Windows PowerShell: + +```powershell +Install-Module -Name Microsoft.PowerApps.Administration.PowerShell +Add-PowerAppsAccount +New-PowerAppManagementApp -ApplicationId CLIENT_ID +``` + +### 5. Give the app access to Cost Management (optional) + +To see Copilot spend, choose the scope your Copilot Studio billing policy and Azure AI resources are billed to, and assign the app the **Cost Management Reader** role there: open the scope in the [Azure portal](https://portal.azure.com), go to **Access control (IAM) → Add role assignment**, and pick the app. Then enter the scope in the **Cost Management scope** field. + +Copilot Studio pay-as-you-go charges land on the subscription that holds the billing policy's **Power Platform account** resource (`Microsoft.PowerPlatform/accounts`). Azure OpenAI and Azure AI Foundry charges land on the subscriptions that hold those resources. To cover several subscriptions at once, use a billing account or billing profile scope. + +## Configuration fields + +| Field | What it is | Where to find it | Required | +| ----- | ---------- | ---------------- | -------- | +| **Directory (tenant) ID** | The Microsoft Entra tenant the app is registered in. | App registration → **Overview**. | Yes | +| **Application (client) ID** | Identifies the app registration. | App registration → **Overview**. | Yes | +| **Client secret** | Authenticates the app. | App registration → **Certificates & secrets**. | Yes | +| **Cost Management scope** | The Azure scope to read Copilot spend from, such as `/subscriptions/` or `/providers/Microsoft.Billing/billingAccounts/`. Leave blank to skip spend data. | Azure portal → the subscription or billing account → **Properties**. | No | + +On save, the plugin checks that it can read licences, Microsoft 365 Copilot usage reports and Copilot Studio credit capacity. A failed licence check means the credentials or Graph permissions are wrong and blocks setup. The other two checks only warn, and name the step to revisit. + +## What this plugin monitors + +- **Licence use**: Copilot seats purchased, assigned and available, so unused seats stand out. +- **Adoption**: enabled and active users per Microsoft 365 app (Teams, Outlook, Word, Excel, PowerPoint, Copilot Chat and more), daily trends, and prompts submitted. +- **Per-user activity**: when each licensed user last used Copilot and in which apps, with users who haven't used it for over 7 or 28 days flagged. +- **Copilot Studio credits**: prepaid capacity, consumption and pay-as-you-go usage this month, and whether the tenant is within capacity or in overage. +- **Spend**: daily Azure costs for Copilot Studio, Copilot Chat pay-as-you-go and Azure AI services, by service, meter or resource. + +The out-of-the-box dashboards include an estate-wide **Overview** plus a perspective for each **Copilot User** and **Environment**. + +## Data streams + +- **Copilot Licences**: purchased, assigned and available seats for each licence that includes Microsoft 365 Copilot (account-wide). +- **Copilot Adoption**: enabled users, active users and active rate per app over a 7, 28, 90 or 180-day period, with prompt totals (account-wide). +- **Copilot Adoption Trend**: daily enabled and active users per app over the chosen period (account-wide). +- **Copilot User Activity**: last activity per app, days since last use, prompts and active days for each licensed user (account-wide, or filtered to selected users). +- **Copilot Credit Capacity**: prepaid and pay-as-you-go Copilot Credit usage this month, with capacity status (account-wide). +- **Environment Credit Allocations**: capacity allocated to an environment, by currency (per environment). +- **Copilot Spend**: daily cost and usage quantity from Azure Cost Management, grouped by service, meter category, meter, resource group, resource or resource type, for Copilot and AI services or all services (account-wide). + +## What gets indexed + +| Object type | API source | Represents | +| ----------- | ---------- | ---------- | +| **Copilot User** | Microsoft Graph `GET /v1.0/users`, filtered to users with an enabled Microsoft 365 Copilot service plan | A user licensed for Microsoft 365 Copilot. | +| **Environment** | Power Platform admin API `GET /providers/Microsoft.BusinessAppPlatform/scopes/admin/environments` | A Power Platform environment, where Copilot Studio agents are built and credits are allocated. | + +## Known limitations + +- **Usage reports lag by about two days**, and use Microsoft's fixed 7, 28, 90 and 180-day periods, chosen with each data stream's **Period** setting rather than the dashboard timeframe. +- **Users who recently lost their licence still appear** in Copilot User Activity: Microsoft's report covers anyone licensed in the last 180 days. +- **Copilot Chat use by unlicensed users isn't reported**: Microsoft Graph only covers users with a Microsoft 365 Copilot licence. +- **Copilot Studio credits are tenant-wide and month to date**: consumption by agent, environment or user, per-agent limits, and session analytics (resolution, escalation, satisfaction) are only in the Power Platform admin center and Copilot Studio. +- **Some environments have no credit allocations**: developer and legacy environments return a "no Copilot credit allocations" message instead of data. +- **Cost Management throttles requests across the whole tenant**, and its data refreshes about every four hours. Several spend tiles refreshing at once may briefly show "Too many requests". +- **Cost Management doesn't support every scope**: management groups return no data for Microsoft Customer Agreement subscriptions, and Azure Sponsorship subscriptions report no costs. Use a subscription, billing account or billing profile scope. +- **Large spend queries can exceed the response size limit**: grouping all services by resource over a whole quarter may fail. Narrow the timeframe or keep the default Copilot and AI services filter. +- **Read-only**: the plugin never changes anything in Microsoft 365, Power Platform or Azure. diff --git a/plugins/MicrosoftCopilot/v1/icon.svg b/plugins/MicrosoftCopilot/v1/icon.svg new file mode 100644 index 00000000..1bdd89ff --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/icon.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/MicrosoftCopilot/v1/indexDefinitions/default.json b/plugins/MicrosoftCopilot/v1/indexDefinitions/default.json new file mode 100644 index 00000000..0d1837ab --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/indexDefinitions/default.json @@ -0,0 +1,42 @@ +{ + "steps": [ + { + "name": "users", + "dataStream": { "name": "users" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "displayName", + "type": { "value": "User" }, + "properties": [ + "userPrincipalName", + "mail", + "department", + "jobTitle", + "officeLocation", + "country", + "accountEnabled" + ] + } + }, + { + "name": "environments", + "dataStream": { "name": "environments" }, + "timeframe": "none", + "objectMapping": { + "id": "name", + "name": "properties.displayName", + "type": { "value": "Environment" }, + "properties": [ + { "environmentType": "properties.environmentSku" }, + { "geography": "location" }, + { "azureRegion": "properties.azureRegion" }, + { "isDefault": "properties.isDefault" }, + { "status": "properties.states.management.id" }, + { "dataverseUrl": "properties.linkedEnvironmentMetadata.instanceUrl" }, + { "createdTime": "properties.createdTime" } + ] + } + } + ] +} diff --git a/plugins/MicrosoftCopilot/v1/metadata.json b/plugins/MicrosoftCopilot/v1/metadata.json new file mode 100644 index 00000000..0fe7592a --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/metadata.json @@ -0,0 +1,57 @@ +{ + "name": "microsoft-copilot", + "displayName": "Microsoft Copilot", + "version": "1.0.0", + "author": { + "name": "SquaredUp Labs", + "type": "labs" + }, + "description": "Track Microsoft 365 Copilot adoption and licence use, Copilot Studio credit capacity, and Copilot spend.", + "category": "Monitoring", + "type": "cloud", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": [ + "copilot", + "copilot studio", + "microsoft 365", + "microsoft", + "ai", + "agents", + "adoption", + "licences", + "credits", + "cost" + ], + "objectTypes": ["User", "Environment"], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/MicrosoftCopilot/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/MicrosoftCopilot/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "https://login.microsoftonline.com/", + "authMode": "none", + "headers": [], + "queryArgs": [], + "showScripting": true, + "scriptingVariables": [ + { "key": "tenantId", "value": "{{tenantId}}" }, + { "key": "clientId", "value": "{{clientId}}" }, + { "key": "clientSecret", "value": "{{clientSecret}}" } + ], + "preRequestScript": "preRequest.js" + } + } +} diff --git a/plugins/MicrosoftCopilot/v1/preRequest.js b/plugins/MicrosoftCopilot/v1/preRequest.js new file mode 100644 index 00000000..2784b36a --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/preRequest.js @@ -0,0 +1,70 @@ +// Copilot data spans several Microsoft APIs, each needing a token for its own +// audience. A pre-request script may only fetch within the base URL's domain, +// so the base URL is the Entra token endpoint's host. Each stream's path then +// starts with an alias for the API it targets (graph/, arm/, powerplatform/, +// bap/), +// which is swapped for that API's real host here. Paging links already carry +// the real host, so they pass straight through. +const hosts = { + graph: "graph.microsoft.com", + arm: "management.azure.com", + powerplatform: "api.powerplatform.com", + bap: "api.bap.microsoft.com", +}; +const audiences = { + "graph.microsoft.com": "https://graph.microsoft.com/.default", + "management.azure.com": "https://management.azure.com/.default", + "api.powerplatform.com": "https://api.powerplatform.com/.default", + "api.bap.microsoft.com": "https://service.powerapps.com/.default", +}; + +if (url.host === "login.microsoftonline.com") { + const [, alias, ...rest] = url.pathname.split("/"); + if (!hosts[alias]) { + api.report.error(`Unknown API alias "${alias}" in ${url.pathname}`); + } + url.host = hosts[alias]; + url.pathname = `/${rest.join("/")}`; +} + +const scope = audiences[url.host]; +if (!scope) { + api.report.error(`No token audience is configured for ${url.host}`); +} + +const now = Date.now(); +const tokens = { ...(state?.tokens ?? {}) }; +const cached = tokens[url.host]; + +if (typeof cached?.token !== "string" || (cached.expiryTime ?? 0) <= now) { + const form = { + grant_type: "client_credentials", + client_id: String(secrets.clientId ?? "").trim(), + client_secret: String(secrets.clientSecret ?? "").trim(), + scope, + }; + const resp = await fetch( + `https://login.microsoftonline.com/${encodeURIComponent(String(secrets.tenantId ?? "").trim())}/oauth2/v2.0/token`, + { + method: "post", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: Object.entries(form) + .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) + .join("&"), + }, + ); + const payload = await resp.json().catch(() => ({})); + if (!resp.ok || typeof payload.access_token !== "string") { + api.report.error( + `Failed to get a token for ${scope}: ${resp.status} ${payload.error_description ?? resp.statusText}`, + ); + } + tokens[url.host] = { + token: payload.access_token, + // Refresh a minute early so a token never expires mid-request + expiryTime: now + (payload.expires_in ?? 3600) * 1000 - 60000, + }; + state = { ...(state ?? {}), tokens }; +} + +headers["Authorization"] = `Bearer ${tokens[url.host].token}`; diff --git a/plugins/MicrosoftCopilot/v1/ui.json b/plugins/MicrosoftCopilot/v1/ui.json new file mode 100644 index 00000000..6cd08c89 --- /dev/null +++ b/plugins/MicrosoftCopilot/v1/ui.json @@ -0,0 +1,36 @@ +[ + { + "type": "text", + "name": "tenantId", + "label": "Directory (tenant) ID", + "placeholder": "00000000-0000-0000-0000-000000000000", + "validation": { + "required": true + } + }, + { + "type": "text", + "name": "clientId", + "label": "Application (client) ID", + "placeholder": "00000000-0000-0000-0000-000000000000", + "validation": { + "required": true + } + }, + { + "type": "password", + "name": "clientSecret", + "label": "Client secret", + "placeholder": "Enter a client secret", + "validation": { + "required": true + } + }, + { + "type": "text", + "name": "costScope", + "label": "Cost Management scope", + "placeholder": "/subscriptions/00000000-0000-0000-0000-000000000000", + "help": "Enables the Copilot spend data stream. Accepts a subscription, resource group, management group or billing account scope. See [Cost Management scopes](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/understand-work-scopes)." + } +]