Table of Contents
Find Obsolete Microsoft 365 Groups and Teams
May 11: Microsoft is deprecating the TechNet Gallery in June 2020. The script can now be downloaded from GitHub.
Oct 14: Microsoft updated the location of the Teams compliance records used to check how active a team is. Version 4.7 or later of the script handles this issue.
I wrote the first version of a script to analyze the activity in Microsoft 365 Groups (then Office 365 Groups) and Teams to identify obsolete groups in 2017. The script is described in this Petri.com article and is reasonably popular. I keep an eye on the feedback from people who run the script and update the script as time goes by. You can download the latest version from GitHub. The latest version is V4.8 dated 16 December 2020.
Update: A Graph-based version of the script is available in GitHub (5.4). This version is much faster at processing Microsoft 365 Groups and Teams and is now the recommended code and the base for future development. See this post for details.
The basic idea is to analyze the Microsoft 365 Groups in a tenant to find underused groups or simply understand the level of activity across groups. The script looks at the level of activity in:
- Conversations stored in the Inbox of group mailboxes (for Outlook Groups).
- Documents (in SharePoint document libraries belonging to the groups)
- Chats (for Teams-enabled Groups). In fact, the script checks the compliance records logged in the group mailbox for conversations in channels belonging to the Teams.
Checking different aspects of individual groups isn’t fast. One tenant tells me that it takes 17 hours to process 5,300 groups… but this isn’t a report that you’ll run daily. It’s more like a monthly or quarterly activity.
Script Outputs
The script records the data for each group in a PowerShell list. Eventually, after processing all the groups, the script outputs an HTML report and a CSV file. The script also assigns a status of Pass, Warning, or Fail to each group depending on the level of detected activity. The determination of the status is entirely arbitrary and should be adjusted to meet the needs of your organization.

At the bottom of the report you’ll see a summary like this:
Report created for: tenant.onmicrosoft.com Number of groups scanned: 182 Number of potentially obsolete groups (based on document library activity): 120 Number of potentially obsolete groups (based on conversation activity): 129 Number of Teams-enabled groups : 65 Percentage of Teams-enabled groups: 35.71% ----------------------------------------------------------------------------
Reviewing the report should help you find the Microsoft 365 Groups and Teams that are not being used. These groups are candidates for removal or archival.
You can view a screen capture video showing how to run the script here.
Recent Improvements
Scripts that evolve over time can often do with a rewrite from scratch. However, I don’t have the time for that. but I do make changes that I think are useful. Here are some recent changes.
- New tests to see if the SharePoint Online and Teams modules are loaded. In particular, the Teams check took far too long because the cmdlets in this module are slow. In fact, the Get-Team module is so slow that we don’t use it from V4.3 onwards. Using Get-UnifiedGroup with a filter is about twice as fast. We might revisit this point as new versions of the Teams PowerShell module appear. See note above about the Graph-based version of the script.
- Use a PowerShell list object to store the report data. This is much faster than an array, especially when you might want to store data for thousands of groups. This was one of the performance tips received after publishing a post about how we write PowerShell and it makes a real difference.
- Use Get-Recipient instead of Get-UnifiedGroup to create a set of group objects to process. Get-Recipient is much faster than Get-UnifiedGroup when you need to create a list of mail-enabled objects like Office 365 Groups. V5.0 and later replaces these calls with Graph API commands.
- Output the storage consumed by the SharePoint site belonging to each group.
- Handle groups that have no conversations in the group mailbox more elegantly. Groups used by Teams are often in this situation.
Test Before Deployment
As always, test any PowerShell code downloaded from the web, even from sources like GitHub, before introducing it to a production system. The code as written needs some extra error handling to make it as robust as it could be, but I’ve left that to the professionals as people tend to have their own way of approaching this issue.
The Office 365 for IT Pros eBook includes many valuable tips for writing PowerShell scripts to interact with Office 365 Groups and Teams. Subscribe to gain benefit from all that knowledge!
Hello all
am getting the below error and CSV and HTML file is null
Results
——-
Number of groups scanned : 0
Potentially obsolete groups (based on document library activity): 0
Potentially obsolete groups (based on conversation activity) : 0
Number of Teams-enabled groups : 9
You cannot call a method on a null-valued expression.
At C:\Users\raarumug\Desktop\MS Teams\Scripts\TeamsGroupsActivityReport4.3.PS1:216 char:1
+ Write-Host “Percentage of Teams-enabled groups …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
Summary report in c:\temp\GroupsActivityReport.html and CSV in c:\temp\GroupsActivityReport.csv
PS C:\Users\raarumug\Desktop\MS Teams\Scripts>
Well, the script hasn’t processed any groups for some reason. What’s in the $TeamList and $Groups variables?
And to make sure that we don’t run into the same problem, I updated the script to V4.4 to include a check that some groups are found before we proceed.
Great script! Have used it regularly to get an idea of the current status of our 3,500+ groups.
I noticed though in the last report that I had a handful of groups reporting SPOStatus as ‘Document library never created’ but SPOActivity was ‘Document library in use’ – which didn’t make a lot of sense!
I found that the $AuditRecs parameter is not set if the $G.SharePointSiteURL does not exist. Thus when it gets set in a loop iteration, on the next iteration the previous value is carried over if $G.SharePointSiteURL does not exist.
I’ve fixed this in my copy of the script, just thought you’d like to know!
Thanks for the note. The script is updated (to V4.6) in GitHub. I hope I made the same change!
Tank you for that Script it worked Great,i have just on Question i have some Groups that Date of chat return null i try to write something but always no informations.
Hi, What exactly do the columns for numbers of warnings and status mean?
Does the script support upto 11000 groups? Because it keeps failing. Any tips?
That’s a lot of groups. At that volume, I would amend the script to use a Graph API command to form the collection of groups to process. See https://office365itpros.com/2020/02/17/paging-teams-from-graph-powershell/ for details.
Iám not realy at home in the scripting scene. How could I do that by adjusting the script?
You’d be creating a new script to remove the calls which form the collection of groups and replacing those commands with Graph API calls as described in the link I referenced. You’d also need to register an app with Azure AD. That process is described in https://petri.com/exploiting-graph-when-powershell-teams. This isn’t probably something to plunge into if you don’t know PowerShell. Is there anyone local within the organization who can help?
I can’t reply on youre last post. But it is a bit over my head that is true. And no there isn’t someone in our organisation with this kind of expertise.
I’ve posted a version of the script which uses the Microsoft Graph in GitHub at https://github.com/12Knocksinna/Office365itpros/blob/master/TeamsGroupsActivityReportV5.PS1. I’m still sorting out some minor issues, but the code works and is three times faster than the previous version. You can try it.
This is a script that is very usefull. But when I run it it keeps failing after a long time. I needs to go true about 11000 groups. Could that be the problem? It takes me about 2 days before it reaches about 3300 groups.
Am I doing something wrong maybe? Tips and tricks are welcome :-).
Hi Tony !
Thank’s for the script.
I want to use the V5 version. I created an app (Azure AD > App registration), but what Microsoft Graph API access does the script need?
Thank you !
I believe the following permissions are used: Group.Read.All, Reports.Read.All
I have some extra permissions assigned to the app I use for testing purposes, but a check of all the Graph calls used indicate that these are the two required.
Thank’s Tony.
Now, I have an issue with the script : Get-GraphData is not recognise (line 119 and 176). Any suggestion ??
Get-GraphData is a function. Did you load the whole script (the function is at the bottom of the code)?
Yes, Get-GraphData is at the bottom of the script. I’m searching… :/
Problem solved :
The Get-GraphData must be in first position (Powershell is not a compiled language).
And i’ve found another “bug”. I’m in a french tenant, so “Shared%documents” is not valid. It’s “Documents%partages” (lines 212 and 213)
I will open a pull to suggest a var named “shareddocs” where users can put there localize “shared%documents”.
Thanks. We write code in the expectation that people take it and adapt for their own use. I’ve moved the function up. I should have done that before. I’ll also fix the shared document issue.
Added update for the URL I18N issue. Here’s the text:
Added the two variables $SharedDocFolder and $SharedDocFolder2
$SharedDocFolder is used to locate the position of the folder for the doclib in a team site. This is language dependent. For example, English language sites have values like:
https://office365itpros.sharepoint.com/sites/euroOffice365Engage/Shared%20Documents, so we look for “/Shared%20Documents”. In French, this is “/Documents%20Partages”
$SharedDocFolder2 holds a value without the %20 (space character) that we append to the site URL to get a well-formed URL to the doclib. For English, this value is “/Shared Documents”. In French, it would be “/Documents Partages”
Hello Tony, Thanx for yoyr work for this script. I tried to get this working for me. Registereing an app is no issue also the permissions are ok. But after running the latest version I still get this error
Extracting list of Microsoft 365 Groups for checking…
Fetching data about Microsoft 365 Groups…
Exception calling “Substring” with “2” argument(s): “Lengte kan niet minder dan nul zijn.
Parameternaam: length”
At C:\tools\TacTix_TeamsGroupActivity_GraphV2.ps1:214 char:8
+ $SPOUrl = $SPOData.WebUrl.SubString(0,$SpoData.weburl.IndexOf( …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentOutOfRangeException
I can’t find where the problem is with this error.
Lengte kan niet minder dan nul zijn = Length cannot be less than zero
I hope you can give me a clue.
Looks like it is failing to get any SharePoint data for the call $Uri = “https://graph.microsoft.com/v1.0/groups/” + $Group.Id + “/drive”
When the script fails, what’s the value of the $SPOData variable?
For what I can see right now there is nothing in the variable.
just to be for sure The only changes I have to make in the script are:
$Appid
$Appsecret
$TenantId
Hello Tony,
I am not a programmer and do not have vast knowledge of PowerShell. I saw this script while searching for a better way to weed out teams that are no longer in use, but am running into some error messages. I hope you can point me in the right direction as, like I stated before, I am not a programmer. It seems that I have to enter a value for the AccessToken before running the script? Below is the output after running the script.
Continuing to fetch information about Microsoft 365 Groups…
Get-GraphData : Cannot bind argument to parameter ‘AccessToken’ because it is null.
At C:\Users\tvanelli\Documents\Office365itpros-master\TeamsGroupsActivityReportV5.PS1:219 char:51
+ $GuestMemberCount = Get-GraphData -AccessToken $Token -Uri $uri
+ ~~~~~~
+ CategoryInfo : InvalidData: (:) [Get-GraphData], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Get-GraphData
Get-GraphData : Cannot bind argument to parameter ‘AccessToken’ because it is null.
At C:\Users\tvanelli\Documents\Office365itpros-master\TeamsGroupsActivityReportV5.PS1:221 char:51
+ $GroupMemberCount = Get-GraphData -AccessToken $Token -Uri $uri
+ ~~~~~~
+ CategoryInfo : InvalidData: (:) [Get-GraphData], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Get-GraphData
Get-Date : Cannot bind parameter ‘Date’ to the target. Exception setting “Date”: “Cannot convert null to type “System.DateTime”.”
At C:\Users\tvanelli\Documents\Office365itpros-master\TeamsGroupsActivityReportV5.PS1:232 char:36
+ WhenCreated = Get-Date ($Group.createdDateTime) -format g
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : WriteError: (:) [Get-Date], ParameterBindingException
+ FullyQualifiedErrorId : ParameterBindingFailed,Microsoft.PowerShell.Commands.GetDateCommand
Get-GraphData : Cannot bind argument to parameter ‘AccessToken’ because it is null.
At C:\Users\tvanelli\Documents\Office365itpros-master\TeamsGroupsActivityReportV5.PS1:242 char:37
+ $Teams = Get-GraphData -AccessToken $Token -Uri $uri
+ ~~~~~~
+ CategoryInfo : InvalidData: (:) [Get-GraphData], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Get-GraphData
No Microsoft 365 Groups can be found – exiting
You’re using the Graph API version of the script, which means that you need to create an app in Azure AD and get an app identifier and secret to use to run the calls to fetch data from the Graph.
Maybe you’d prefer to run the pure PowerShell version at https://github.com/12Knocksinna/Office365itpros/blob/master/TeamsGroupsActivityReport.ps1. It’s slower, but easier to run.
Ahhh. Thanks very much. I was wondering if that was the case after reading some of the other comments here, but I wasn’t sure I even understood what I was reading, haha. I would eventually like to learn how to create apps in Azure as it seems like a good skill to have if I’m going to be our Teams admin.
The script is running like a champ right now. Thanks again.
The script ran great and output the information needed.
Are the .html and .csv file saved at the end? I get the message saying that the summary report and CSV are in c:\temp, but there is no such directory.
I’m sure that I’m missing something again. If these files are not saved automatically, is there a way of saving the output file that is generated?
You’ll need to either create the c:\temp directory or change the script to point to another directory. If the directory isn’t available, the files aren’t created. However, you should have two PowerShell objects that can be exported (if the session is still available). Try:
$htmlreport | Out-File c:\htmlreport.html-Encoding UTF8
$Report | Export-CSV -NoTypeInformation c:\report.csv
Replace the file names I use with your preference and you should be able to generate the files.
pulling down the latest version of the script, when I run it, it comes up saying that there is a error on line 237 with an extra } barket. once this is remvoed, and I have added my Application registration details, the script runs but comes back with “Fetching data about Microsoft 365 Groups…
No Microsoft 365 Groups can be found – exiting”
There was an extra bracket, which I removed.
It was after this section:
$Uri = “https://graph.microsoft.com/v1.0/groups/” + $Group.Id + “/owners?”
$GroupData = Invoke-RestMethod -Headers $Headers -Uri $Uri -UseBasicParsing -Method “GET”
If ($GroupData.”@odata.count” -eq 0) {
$OwnerNames = “No owners found” }
Else { # Extract owner names
$OwnerNames = $GroupData.Value.DisplayName -join “, ” }
Did you remove the same bracket? The code now posted in GitHub works.
Just like Matthew C I still have the same issue.
Did you fetch the updated script from GitHub?
Yes, I have downloaded the latest version. I don’t have the error anymore from the bracket.
Thanks for all the great help, Tony. I got the script to run and save the files where I wanted. I’m happy I came across this when I did because it was much needed.
Out of curiosity, is this supposed to be able to run from PowerShell ISE? I tried to run it 4 times on two different machines yesterday. Three times on my local PC while working from home connected to VPN and once from a PC at work over an RDP session. Each time PowerShell ISE froze (at different times in the script) and I received a generic “…encountered a problem” or something, error message behind the PowerShell window. I then ran it from regular PowerShell at home over VPN today and it worked fine.
Just wondering…
I don’t use the PowerShell ISE so I can’t vouch for it.
Tony, Thank you for this script, and for your frequent updates!
Now running v5.1 of the Graph-based script, I noticed a couple of differences in the results from when I was running v4.8 of the orginal script, and wanted to share them with you.
The first is that for my >3700 Groups, the report lists 0 for “number of Teams-enabled groups”, while the v4.8 script shows 327 teams across a small sample of 375 groups I let that script process.
The second item is that the report returns no results for SPOStorageGB (empty or null) while the v4.8 script returns data for each row.
Again, thank you for providing these scripts!!
Can you run the start of the script to the point where it fetches the set of Teams to build the Teams hash table and see what’s returned when $TeamsCount = $TeamsHash.count. That’s how the number of team-enabled groups is reported. As to SharePoint, the storage data comes from the call: $SPOUsage = (Invoke-RestMethod -Uri $SPOUsageReportsURI -Headers $Headers -Method Get -ContentType “application/json”) -Replace “…Report Refresh Date”, “Report Refresh Date” | ConvertFrom-Csv
Check what comes back to $SPOUsage.
I don’t see a problem in either place here. It all works as expected.
Will do. Be back as soon as possible.
Tony, The first time I ran the code the CLS near the end masked me seeing the errors. I set a breakpoint on line 249. I can see that $GroupsList contains 3,740 entries while $Teams contains a single record.
I am logged in as a global admin, but from the errors shown it appears there are some other permission issues. I can confirm I have granted the permissions you noted in an earlier comment to my app (Group.Read.All, Reports.Read.All).
I would welcome any feedback you may have. I renamed your script, as noted in the errors, with “(tony)” simply so I would know I was running your original unaltered version.
Continuing to fetch information about Microsoft 365 Groups…
Get-GraphData : System.Net.WebException: The remote server returned an error: (401) Unauthorized.
at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.GetResponse(WebRequest request)
at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord()
At C:\Users\michael.sevast\Desktop\PowerShell\TeamsGroupsActivityReportV5(tony).ps1:218 char:24
+ $GuestMemberCount = Get-GraphData -AccessToken $Token -Uri $uri
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-GraphData
Get-GraphData : System.Net.WebException: The remote server returned an error: (401) Unauthorized.
at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.GetResponse(WebRequest request)
at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord()
At C:\Users\michael.sevast\Desktop\PowerShell\TeamsGroupsActivityReportV5(tony).ps1:220 char:24
+ $GroupMemberCount = Get-GraphData -AccessToken $Token -Uri $uri
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-GraphData
Get-GraphData : System.Net.WebException: The remote server returned an error: (401) Unauthorized.
at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.GetResponse(WebRequest request)
at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord()
At C:\Users\michael.sevast\Desktop\PowerShell\TeamsGroupsActivityReportV5(tony).ps1:241 char:10
+ $Teams = Get-GraphData -AccessToken $Token -Uri $uri
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-GraphData
Exception calling “Add” with “2” argument(s): “Key cannot be null.
Parameter name: key”
At C:\Users\michael.sevast\Desktop\PowerShell\TeamsGroupsActivityReportV5(tony).ps1:244 char:4
+ $TeamsHash.Add($_.Id, $_.DisplayName) } )
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentNullException
Can you try adding the GroupMember.Read.All permission? I see that it’s failing to return the counts for guest and tenant users. That could be it.
Tony,
I forgot I had already added GroupMember.Read.All during my testing. I deleted and readded it just to be sure. To confirm, Group.Read.All, Reports.Read.All, and GroupMember.Read.All are the only permissions assigned to my app. Unfortunately, my results are the same as I showed previously.
Do you have User,Read.All? I have that and User.Read in the app I am using. I can’t recall if this was done for a reason. It’s a while since I set the app up.
Tony, I’m baffled as I’ve deleted all permissions and then added these permissions; Group.Read.All, GroupMember.Read.All, Reports.Read.All, Sites.Read.All, Team.ReadBasic.All, TeamMember.Read.All), but the same errors continue to be displayed. I’m about ready to grant the app full “kitchen sink” permissions 😉 Seriously, I appreciate your feedback so far. This could be a “between the ears” issue on my end. I’m just not seeing it.
Shorted my list a bit. The full permissions granted are Group.Read.All, GroupMember.Read.All, Reports.Read.All, Sites.Read.All, Team.ReadBasic.All, TeamMember.Read.All, User.Read.All and User.Readwrite.All.
It’s odd… You seem to have the right permissions but you don’t… Never mind, we shall work the issue through and get a successful conclusion.
I am also having SPOStorageGB return 0GB but it tracks further back to this loop:
ForEach ($Site in $SPOUsage) {
If ($Site.”Root Web Template” -eq “Group”) {
If ([string]::IsNullOrEmpty($Site.”Last Activity Date”)) { # No activity for this site
$LastActivityDate = $Null }
Else {
$LastActivityDate = Get-Date($Site.”Last Activity Date”) -format g
$LastActivityDate = $LastActivityDate.Split(” “)[0] }
$SiteDisplayName = $Site.”Owner Display Name”.IndexOf(“Owners”) # Extract site name
If ($SiteDisplayName -ge 0) {
$SiteDisplayName = $Site.”Owner Display Name”.SubString(0,$SiteDisplayName) }
Else {
$SiteDisplayName = $Site.”Owner Display Name” }
$StorageUsed = [string]([math]::round($Site.”Storage Used (Byte)”/1GB,2)) + ” GB”
$SingleSiteData = @{
‘DisplayName’ = $SiteDisplayName
‘LastActivityDate’ = $LastActivityDate
‘FileCount’ = $Site.”File Count”
‘StorageUsed’ = $StorageUsed }
$DataTable.Add([String]$Site.”Site URL”,$SingleSiteData)
}
}
It looks like the issue is the first If statement, “If ($Site.”Root Web Template” -eq “Group”) {”
As a lot of the Root Web templates are not Group for mine, but rather “Team Site” so this ends up with no information in the datatable
Changing it to “If ($Site.”Root Web Template” -eq “Group” -Or “Team Site”) {” enabled this to then get most sites/teams data correctly, removing this If completely has meant that all SPO sites have complete data in the datatable and then only gets used if needed later on in the script, and dosnt seem to add any running time to the script.
Thanks
On the script finishing running it seems that it still has not output the SPOStorageGB but it is listed in the Data table correctly now
the issue looks to be on line 413: SPOStorageGB = $SPOStorage should be SPOStorageGB = $SPOStorageUsed
Here’s how it should work:
SPO data is collected into a hash table, including the storage used
$SingleSiteData = @{
‘DisplayName’ = $SiteDisplayName
‘LastActivityDate’ = $LastActivityDate
‘FileCount’ = $Site.”File Count”
‘StorageUsed’ = $StorageUsed }
The $SPOStorageUsed variable is filled using a lookup against the hash table containing SPO site information.
$ThisSiteData = $Datatable[$G.SharePointURL]
$SPOFileCount = $ThisSiteData.FileCount
$SPOStorageUsed = $ThisSiteData.StorageUsed
And is later written out into the report:
“SPO Storage Used (GB)” = $SPOStorageUsed
But I notice that my copy of the script has this code and the version on GitHub uses what you report, so that’s a bug (now fixed). Thanks for letting me know.
Hi,
that’s a great script.
I add a new Application (App Registration) give them a lot of permission. (Group.Read.All, GroupMember.Read.All, Reports.Read.All, Sites.Read.All, Team.ReadBasic.All, TeamMember.Read.All, User.Read.All and User.Readwrite.All.)
But when i run your script i get error:
Checking Microsoft 365 Groups and Teams in the tenant: xxxxx.onmicrosoft.com
Invoke-RestMethod : The remote server returned an error: (403) Forbidden.
At C:\Users\a-00114099\Desktop\test.ps1:164 char:17
+ … SPOUsage = (Invoke-RestMethod -Uri $SPOUsageReportsURI -Headers $Head …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebExc
eption
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand
Fetching data about Microsoft 365 Groups…
Get-GraphData : System.Net.WebException: The remote server returned an error: (403) Forbidden.
at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.GetResponse(WebRequest request)
at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord()
At C:\Users\a-00114099\Desktop\test.ps1:192 char:11
+ $Groups = Get-GraphData -AccessToken $Token -Uri $uri
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-GraphData
Get-GraphData : System.Net.WebException: The remote server returned an error: (400) Bad Request.
at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.GetResponse(WebRequest request)
at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord()
At C:\Users\a-00114099\Desktop\test.ps1:197 char:17
+ $GroupData = Get-GraphData -AccessToken $Token -Uri $Uri
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Do you know what’s the problem?
You’re hitting an error when fetching the SharePoint usage data and then when fetching Groups. Has consent been given by an admin to allow Reports.Read.All and Groups.Read.All and GroupsMember.Read.All to be used? Is this a test tenant where report data isn’t available by any chance?
Thank’s for your fast reply!
I give them all permissions –> https://ibb.co/z8zNWXn
Where can i check if the reporting is disabled?
Thank you Tony!
Ahhh, now it’s working. Took some time after adding the permissions. But now i get another error 🙂
Do you know what the problem is?
Checking Microsoft 365 Groups and Teams in the tenant: xxxx.onmicrosoft.com
Exception calling “Add” with “2” argument(s): “Item has already been added. Key in dictionary:
‘https://xxx.sharepoint.com/sites/WO-Bereich-V’ Key being added:
‘https://xxx.sharepoint.com/sites/WO-Bereich-V'”
At C:\Users\a-00114099\Desktop\test.ps1:184 char:6
+ $DataTable.Add([String]$Site.”Site URL”,$SingleSiteData)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentException
Fetching data about Microsoft 365 Groups…
Exception calling “Substring” with “2” argument(s): “Length cannot be less than zero.
Parameter name: length”
At C:\Users\a-00114099\Desktop\test.ps1:211 char:8
+ $SPOUrl = $SPOData.WebUrl.SubString(0,$SpoData.weburl.IndexOf( …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentOutOfRangeException
Exception calling “Substring” with “2” argument(s): “Length cannot be less than zero.
Parameter name: length”
At C:\Users\a-00114099\Desktop\test.ps1:211 char:8
+ $SPOUrl = $SPOData.WebUrl.SubString(0,$SpoData.weburl.IndexOf( …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentOutOfRangeException
Exception calling “Substring” with “2” argument(s): “Length cannot be less than zero.
Parameter name: length”
At C:\Users\a-00114099\Desktop\test.ps1:211 char:8
+ $SPOUrl = $SPOData.WebUrl.SubString(0,$SpoData.weburl.IndexOf( …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentOutOfRangeException
Looks like you didn’t initialize all the variables before running the script with the right permissions. When the script attempts to populate hash tables holding data, it finds that the data it wants to add is already there and you get the problem.
is there anything that needs to be done to “fix” this issue after sorting out the permissions?
What version of the script are you trying to use? The current version (5.4) is Graph-based. Seehttps://office365itpros.com/2022/03/14/microsoft-365-groups-teams-activity-report/ for details.
I am using the version 5.4
Teams and Groups Activity Report V5.4 starting up…
The Teams usage data file c:\temp\TeamsUsageData.csv is dated 2022/03/08 15:55 and will be used.
Exception calling “Add” with “2” argument(s): “Item has already been added. Key in dictionary: ‘https://xxxxx.sharepoint.com/sites/Orion’ Key
being added: ‘https://xxxxx.sharepoint.com/sites/Orion'”
At C:\temp\TeamsGroupsActivityReportV5.PS1:254 char:6
+ $DataTable.Add([String]$Site.”Site URL”,$SingleSiteData)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentException
Checking Microsoft 365 Groups and Teams for: xxxxx.onmicrosoft.com
The error that you’re seeing is in the function which builds the hash table of SharePoint site information. It’s complaining that an attempt is being made to add a site to the table that already exists in the table. Can you run this code to see what ends up in the $DataTable (hash table) compared with the information in the $SPOUsage array?
$SPOUsageReportsURI = “https://graph.microsoft.com/v1.0/reports/getSharePointSiteUsageDetail(period=’D90′)”
$SPOUsage = (Invoke-RestMethod -Uri $SPOUsageReportsURI -Headers $Headers -Method Get -ContentType “application/json”) -Replace “…Report Refresh Date”, “Report Refresh Date” | ConvertFrom-Csv
$DataTable = @{} # Build hashtable with SharePoint usage information per site
ForEach ($Site in $SPOUsage) {
If ($Site.”Root Web Template” -eq “Group”) {
If ([string]::IsNullOrEmpty($Site.”Last Activity Date”)) { # No activity for this site
$LastActivityDate = $Null }
Else {
$LastActivityDate = Get-Date($Site.”Last Activity Date”) -format g
$LastActivityDate = $LastActivityDate.Split(” “)[0] }
$SiteDisplayName = $Site.”Owner Display Name”.IndexOf(“Owners”) # Extract site name
If ($SiteDisplayName -ge 0) {
$SiteDisplayName = $Site.”Owner Display Name”.SubString(0,$SiteDisplayName) }
Else {
$SiteDisplayName = $Site.”Owner Display Name” }
$StorageUsed = [string]([math]::round($Site.”Storage Used (Byte)”/1GB,2)) + ” GB”
$SingleSiteData = @{
‘DisplayName’ = $SiteDisplayName
‘LastActivityDate’ = $LastActivityDate
‘FileCount’ = $Site.”File Count”
‘StorageUsed’ = $StorageUsed }
$DataTable.Add([String]$Site.”Site URL”,$SingleSiteData)
}
Josef, did you manage to solve this issue. I’m getting the same when I try to run the Graph-version.
Regards,
Henrik
Try changing the call from Get-MailboxFolderStatistics to Get-ExoMailboxFolderStatistics. The new REST-based cmdlet should be more resistant to these kind of transient errors.
Hi Tony!
I have noticed that if a team has a Private Channel the value of LastConveration is not updated anymore. The script exports the date when the last conversation was made before the first Private Channel was created. Is this a know issue? Any advice for a solution?
Regards,
Henrik
What version of the script are you running?
I’m using the PowerShell version, v4.6. And it is the LastChat ($LastItemAddedtoTeams) that is not updated, LastConversation is working fine.
I will try v4.8 instead, didn’t realize that I was using an old version.
Feel free to fix the problem if you find one…
Hi again!
I have tested v4.8 and it gives the correct values in LastChat, thank you for the update of the script. But for many teams/groups it gives an error message:
Error on proxy command ‘Get-MailboxFolderStatistics -Identity:’bd5a1695-97bc-48f3-839a-22a49e0dcb7b’ -IncludeOldestAndNewestItems:$True -FolderScope:’NonIpmRoot” to server HE1PR0401MB2521.eurprd04.prod.outlook.com: Server version 15.20.4020.0000, Proxy method PSWS:
Cmdlet error with following error message: Microsoft.Exchange.Data.Directory.ADServerSettingsChangedException: An error caused a change in the current set of domain controllers.. [Server=VI1PR04MB4174,RequestId=a03427af-4598-4b87-b64c-622ac521acfe,TimeStamp=2021-04-15 06:13:15] .
+ CategoryInfo : NotSpecified: (:) [Get-MailboxFolderStatistics], CmdletProxyException
+ FullyQualifiedErrorId : [Server=VI1PR04MB4174,RequestId=a03427af-4598-4b87-b64c-622ac521acfe,TimeStamp=2021-04-1 5 06:13:15] [FailureCategory=Cmdlet-CmdletProxyException] 8927B4A,Microsoft.Exchange.Management.Tasks.GetMailboxFolderStatistics + PSComputerName : outlook.office365.com
Error on proxy command ‘Get-MailboxFolderStatistics -Identity:’bd5a1695-97bc-48f3-839a-22a49e0dcb7b’ -IncludeOldestAndNewestItems:$True -FolderScope:’ConversationHistory” to server HE1PR0401MB2521.eurprd04.prod.outlook.com: Server version 15.20.4020.0000, Proxy method PSWS:
Cmdlet error with following error message: Microsoft.Exchange.Data.Directory.ADServerSettingsChangedException: An error caused a change in the current set of domain controllers.. [Server=VI1PR04MB4174,RequestId=af88cc9e-d2ed-45e7-805e-3a803bf2a47e,TimeStamp=2021-04-15 06:13:16] .
+ CategoryInfo : NotSpecified: (:) [Get-MailboxFolderStatistics], CmdletProxyException
+ FullyQualifiedErrorId : [Server=VI1PR04MB4174,RequestId=af88cc9e-d2ed-45e7-805e-3a803bf2a47e,TimeStamp=2021-04-15 06:13:16] [FailureCategory=Cmdlet-CmdletProxyException] 3BBC97F7,Microsoft.Exchange.Management.Tasks.GetMailboxFolderStatistics + PSComputerName : outlook.office365.com
I tried running v4.6 at the same time and it run without any error messages. I don’t know much about scripting, but I guess that there is a difference in how the two versions are extraction the information. Based on the error message, can you see what the issue might be and give me a hint :-).
Thanks!
You hit a transient error in Exchange Online PowerShell: An error caused a change in the current set of domain controllers..
This is one reason why I am gradually changing the code to use Graph API calls.
OK, thanks! I will see if I can get the Graph version to run.
Tony,
First let me say, I am a newbie to PowerShell.
I need your help to modify this script to use App-Only connection to MS Graph using SSL Certificate.
Thanks.
Welcome to PowerShell. I don’t know your environment or your tenant, so I am afraid that I can’t help you. I write scripts to illustrate principles. After that, it’s up to individual tenants to integrate the principles into their production code. If you need help, you can ask in the Microsoft Technical Community or engage a professional programmer. That’s usually the best course to take.
I Hello, I am executing the script and the result teams enable always returns false, which is the criterion by which it decides if teams enable is true or false?
Which version of the script are you using?
I’m using # TeamsGroupsActivityReportV5.PS1
There’s a hash table called $TeamsHash which has an entry for each team-enabled group in the tenant. The check in
If ($TeamsHash.ContainsKey($G.ObjectId) -eq $True) {$GroupIsTeamEnabled = $True}
looks for a match in the Teams hash table using the group identifier (GUID). If it is found, the group is team-enabled. If not, it’s not. The code works for me (as always!).
Maybe you should look at what’s in $TeamsHash and try to do some lookups for groups you know are team-enabled to see what results you get.
What is the difference between the number of conversations and the number of chats?
A Microsoft 365 group can be used for different forms of collaboration. Number of chats = Teams messages. Number of conversations = Outlook conversations.
Hey Tony,
the v5 script worked great. it was as easy as
– registering an app in Azure.
– providing it the necessary permissions (https://ibb.co/z8zNWXn)
– updating the values within the script (app ID, tenant ID & secret)
and boom.
Everything ran according to plan.
Thank you for creating this.
I wanted to know how I can further use this. For example, I will like to get the same details provided in the table but for each channel within a team.
any idea how to go about doing that or modifying the script for that.
I am not sure exactly where to start, maybe Get-TeamChannel? any help would be welcomed.
thank you.
Given that it’s now a Graph-based script and you have all the necessary identifiers for each team, you can use the Graph to fetch details of channels. Try this article for some inspiration: https://petri.com/exploiting-graph-when-powershell-teams
great. I will give that a try and report back on any progress.
thank you.
Can you please provide us the powershell script using Graph to export data only for Teams user activity. That will be of great help.
I could, but you would learn more by changing the PowerShell code yourself – and anyway, you’ll need to do this to make sure that the code runs properly and complies with the standard required by your organization. But I might start with the user activity script instead https://office365itpros.com/2020/09/14/speed-powershell-hash-tables-office365-data/
Great script Tony, It ran for 8 hours to get info about our 5500 Teams in our Educational tenant. Thank you!
Which version did you use? Pure PowerShell or PowerShell/Graph API? I would hope that the Graph version would process the groups faster.
Great script and I runt it regularly to check our environment to see how it looks. I have also used parts of it to develop automations and one thing I noticed then. When you check for last chat message and chat message count private channels are not included, meaning if there are teams where the main work is done in private channels you will not know they are active.
Do you know if that is solvable?
Compliance records for private channel messages are logged in the personal mailboxes of channel members. They’d be counted along with other chats.
Thanks!
So to be able to find if a Team is active or not I also have to check the members of the private channels mailboxes. How can you see what is from a private channel chat and what is a one to one chat.
What I try to achieve is to get a report a Teams that are truly inactive. Maybe it is not possible
There are MAPI properties which reveal if a compliance record is for a personal chat or a private channel conversation. However, I don’t think these are easily accessed with PowerShell. We’re going to have the same issue with shared channels as the compliance records for their conversations are in cloud-only mailboxes inaccessible to PowerShell. I’m not sure what to do here… need to think some more about the issue!
More details about the MAPI properties for the compliance records captured for Teams private channel conversations are in https://office365itpros.com/2020/02/05/teams-private-channel-holds/
To bad, feels like a dead end. This will kill effectively all applications out there that will check for inactive teams as they can not be sure as soon as there is an private channel. Hopefully Microsoft at least will make the “Teams usage” report in admin center available from Graph. I haven’t verified though that the “Channel Messages” column also include private channels, they may face the same problem 🙂
One way to tackle the problem is to use the List Messages API. Unfortunately, this is a “protected API”, which means that you need Microsoft permission to use it: https://docs.microsoft.com/en-us/graph/teams-protected-apis
I verified the channel messages count in the Teams usage report also includes messages in private channels, wonder what is preventing Microsoft from giving access to that report from Graph.
Hi there, can you confirm if we also need an account with exchange admin permissions as well as the app registration mention above.
The Graph-based version uses Graph permissions assigned to a registered app.
The pure PowerShell-based version uses Exchange administrative permissions, so you need to run it using an account that holds an Exchange admin role.
Thanks for your prompt response 🙂
The Graph version of your script (TeamsGroupsActivityReportV5.PS1) checks for a connection to ExchangeOnlineManagement as the first thing that it does in lines 134-138
https://github.com/12Knocksinna/Office365itpros/blob/master/TeamsGroupsActivityReportV5.PS1
Am I looking at the wrong script?
Thanks again,
Dan
Hi Tony, thanks for the script!
I’m using the v5.6, just recently updated by you.
At the beginning I get this error:
Exception calling “Add” with “2” argument(s): “Item has already been added. Key in dictionary:
‘site-XXXXXXXXXXXXXXXXXXXXX’ Key being added:
‘site-XXXXXXXXXXXXXXXXXXXXX'”
At C:-scriptLocation-XXXXXXXXXX.ps1:252 char:6
+ $DataTable.Add([String]$Site.”Site URL”,$SingleSiteData)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentException
AAD Service Principal permissions are set: https://ibb.co/0Jdbw0j
Then I get these exceptions below.
I will try to reproduce the issue in other tenant and see.
Any ideas about this though?
Get-GraphData : System.Net.WebException: The remote server returned an
error: (401) Unauthorized.
at
Microsoft.PowerShell.Commands.WebRequestPSCmdlet.GetResponse(WebRequest
request)
at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord()
At XXXXXXXXX.ps1:305 char:24
+ $GuestMemberCount = Get-GraphData -AccessToken $Token -Uri $uri
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorE
xception
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExce
ption,Get-GraphData
Get-GraphData : System.Net.WebException: The remote server returned an
error: (401) Unauthorized.
at
Microsoft.PowerShell.Commands.WebRequestPSCmdlet.GetResponse(WebRequest
request)
at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord()
At XXXXXXXXXXXX.ps1:307 char:24
+ $GroupMemberCount = Get-GraphData -AccessToken $Token -Uri $uri
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorE
xception
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorExce
ption,Get-GraphData
This error is caused by a failure to add data for a SharePoint site to the hash table which stores details of SharePoint site usage. I have no idea why this might be a problem because the only change in 5.6 is to increase the period of days back we look for usage data from 90 to 180 days.
Exception calling “Add” with “2” argument(s): “Item has already been added. Key in dictionary:
‘site-XXXXXXXXXXXXXXXXXXXXX’ Key being added:
‘site-XXXXXXXXXXXXXXXXXXXXX’”
This error is a permissions problem. It means that the access token passed by the Get-GraphData function doesn’t include the permissions to fetch the requested data. Line 305 is where the guest member count for a group is fetched. We haven’t changed the permission needed for this call (Group.Read.All).
Get-GraphData : System.Net.WebException: The remote server returned an
error: (401) Unauthorized.
Hi Tony,
I used the old Exchange powershell script V5 and that worked. I’m trying out your graph API script on a Test tenant and I’m getting errors below. I’ve created an app registration and gave it Mail.Read, Reports.Read.All, User.Read, User.Read.All, Group.Read.All, Sites.Read.All, GroupMember.Read.All. I’ve also added my app id, tenant id, and appsecret. Thanks for your help.
Write-Error: C:\Scripts\Test\TeamsGroupsActivityReportV5.8.PS1:223
Line |
223 | $OrgData = Get-GraphData -Uri $Uri -AccessToken $Token
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Response status code does not indicate success: 403 (Forbidden).
Write-Error: C:\Scripts\Test\TeamsGroupsActivityReportV5.8.PS1:129
Line |
129 | [array]$TeamsData = Get-GraphData -Uri $Uri -AccessToken $Token
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Response status code does not indicate success: 403 (Forbidden).
Retrieving SharePoint Online site usage data…
Invoke-RestMethod: C:\Scripts\Test\TeamsGroupsActivityReportV5.8.PS1:237
Line |
237 | … SPOUsage = (Invoke-RestMethod -Uri $SPOUsageReportsURI -Headers $Head …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| {“error”:{“code”:”UnknownError”,”message”:”{\”error\”:{\”code\”:\”S2SUnauthorized\”,\”message\”:\”Invalid
| permission.\”}}”,”innerError”:{“date”:”2022-12-16T20:28:56″,”request-id”:”5f6122bb-69d6-44d8-8a09-8c07030d7336″,”client-request-id”:”5f6122bb-69d6-44d8-8a09-8c07030d7336″}}}
Checking Microsoft 365 Groups and Teams for:
This phase can take some time because we need to fetch every group in the organization and
then analyze its settings and activity. Please wait.
Write-Error: C:\Scripts\Test\TeamsGroupsActivityReportV5.8.PS1:274
Line |
274 | [array]$Groups = Get-GraphData -AccessToken $Token -Uri $uri
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Response status code does not indicate success: 403 (Forbidden).
Can’t find any Microsoft 365 Groups – Check your access token
403 errors mean that the access token presented doesn’t include the necessary permission to access data. In the first case ($OrgData), it’s probably because the app hasn’t been assigned the Organization.Read.All or Directory.Read.All permission (see https://learn.microsoft.com/en-us/graph/api/organization-get?view=graph-rest-1.0&tabs=http).
The 403 errors with $TeamsData and $SPOUsage indicate that the app doesn’t have the Reports.Read.All permission (see https://learn.microsoft.com/en-us/graph/api/reportroot-getteamsteamactivitydetail?view=graph-rest-beta). Check the access token (see https://office365itpros.com/2022/02/17/understanding-azure-ad-access-token/) to make sure that the permissions are present.
Thanks for your help. I was able to fix it from changing the app permission for Microsoft Graph from delegated to application. Now it is running without any errors.
That would do. Delegated permissions mean that you’re running as the signed in user. You can access whatever that user can access and only that…
Hi Tony, thank you for your continued development of this script and all the support you’ve given us over the years via these comments.
I’m trying to run the latest Graph-based version of this script (pulled from GitHub today) and I’m unfortunately I’m not seeing Teams messaging dates or counts, inbox activity dates or counts, SPO activity or storage or file counts, etc. It feels like this should be a permissions issue, but I’ve added a long list of Microsoft Graph application permissions so I’m not sure what I’m missing. Here’s the list, can you help? THANK YOU!
– AuditLog.Read.All
– ChannelSettings.Read.All
– ChatMessage.Read.All
– Files. Read.All
– Group.Read.All
– GroupMember.Read.All
– Mail.Read
– MailboxSettings.Read
– Organization.Read.All
– PeopleSettings.Read.All
– Repofts.Read.All
– ReportSettings.Read.All
– SharePoint TenantSettings.Read.All
– Sites.Read.All
– Team.ReadBasic.All
– TeamMember.Read.All
– TeamsActivity.Read.All
– TeamSettings.Read.All
– User.Read.All
make sure that you’re using application permissions and not delegate permissions. You need application permission for the script to access all the group data.
Thank you Tony! Yes, they are definitely application permissions. See: https://ibb.co/P6CZfd0
OK, then you’re back to needing to debug individual Graph calls to see what works and what doesn’t. For example, if you select a group and populate the $Group variable and run the different commands, do you see data returned?
Hello Tony
Great script!! It is working for me with these two errors: Not sure if I should ignore
Exception calling “Add” with “2” argument(s): “Item has already been added. Key in dictionary: ‘00000000-0000-0000-0000-000000000000’ Key being added: ‘00000000-0000-0000-0000-000000000000′”
At line:182 char:4
+ $TeamsDataHash.Add([string]$Team.TeamId, $DataLine)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentException
Exception calling “Add” with “2” argument(s): “Item has already been added. Key in dictionary: ‘00000000-0000-0000-0000-000000000000’ Key being added: ‘00000000-0000-0000-0000-000000000000′”
At line:182 char:4
+ $TeamsDataHash.Add([string]$Team.TeamId, $DataLine)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentException
Exception calling “Add” with “2” argument(s): “Item has already been added. Key in dictionary: ‘00000000-0000-0000-0000-000000000000’ Key being added: ‘00000000-0000-0000-0000-000000000000′”
At line:182 char:4
+ $TeamsDataHash.Add([string]$Team.TeamId, $DataLine)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentException
Most importantly,
Like to get Teams and group members in my report like you did for group owners
Like to get group email in my report
Please let me know
The errors are when the script attempts to add a record to the $TeamsDataHash hash table and finds that the key already exists. I don’t know why this is happening and I don’t have access to your tenant so you’ll have to debug it there.
As to adding members in the report, the script is all yours… go for it and code away. There are plenty of examples available about how to extract this information (like https://office365itpros.com/2023/01/19/microsoft-365-groups-report-v2/).
Abdul, As a quick suggestion, you might also verify that setting “Display concealed user, group, and site names in all reports” is unchecked under the Admin Center\Settings\Org Settings\Services\Reports settings. I recall this being the reason I received this same error. Once it was unchecked, I got back the correct Team IDs.
Hello Tony,
Errors are gone after unchecked. Thank you. I was able to get Members and groupmail to my report but struggling with Owners count
#$Uri = “https://graph.microsoft.com/v1.0/groups/” + $Group.Id + “/owners/?$count=true & ConsistencyLevel:eventual&$select=id”
#$Uri = “https://graph.microsoft.com/v1.0/groups/” + $Group.Id + “/owners/’$count?’”
#$OwnersData = Get-GraphData -AccessToken $Token -Uri $uri
Will you please help me?
Thank you so much
What happens when you run the query in the Graph Explorer?
I got it working.
Thank you.
Hello Tony,
How can I get original creator /author of a Group/Team?
Thank you
Identifying the creator of an Microsoft Team (Who created this Team)? How can I achieve this?
Regards
Abdul
You’d have to check the audit record noting the creation of the team. That is, if the audit record is still available.
Hi Tony
Thank you for getting back. Is there any PowerShell command I can incorporate with this report?
Not easily. As I noted, you’d have to find the creator name from somewhere. If this is a big requirement, I might:
Create a file logging group id, group name, person who created the group, and creation date time.
Use a scheduled task (Azure Automation would be a good choice for this) to scan the audit log every week for new groups and update the file.
When running the report, load the details from the file into an array (or hash table) and look up the details as each group is processed.
There’s some work to do if you want to include this information in the report.
Hi Tony, Thanks for your prompt response. I sort of get it working by using below but giving me all 1’s for all groups which is not right
$Uri = “https://graph.microsoft.com/beta/groups/” + $Group.Id + “/Owners/Microsoft.Graph.User/`$count?`$filter=UserType eq ‘Member'”
$OwnersCount = Get-GraphData -AccessToken $Token -Uri $uri
Owners OwnersCount
Abdul Afrad 1
Abdul Afrad 1
Abdul Afrad 1
Abdul Afrad 1
Abdul Afrad 1
Abdul Afrad 1
Abdul Afrad 1
Abdul Afrad, Test Abdul Afrad 1
for an example last one should be 2
Hi Tony,
I run the report and have some teams reported the team member is 0 but i checked on the Teams admin, we have team members assigned to the those teams.
Happy to send you screenshot throught email
Thanks
A screenshot doesn’t really tell me anything. Also, I don’t work on the old (PowerShell-based) version of the script any more because it is so slow. The new Graph based version https://github.com/12Knocksinna/Office365itpros/blob/master/TeamsGroupsActivityReportV5.PS1 is what you should be using. Is that it>
Hello,
I am running the graph version and have created the app and given it all the mentioned application permissions.
I am getting the error below
Retrieving SharePoint Online site usage data…
Invoke-RestMethod: C:\Users\odulajb\OneDrive – VolkerWessels UK\Documents\Scripts\TeamsGroupsActivityReportV5.PS1:252
Line |
252 | … SPOUsage = (Invoke-RestMethod -Uri $SPOUsageReportsURI -Headers $Head …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| { “error”: { “code”: “InvalidAuthenticationToken”, “message”: “Access token is empty.”,
| “innerError”: { “date”: “2024-03-20T12:17:57”, “request-id”: “3f28d20d-67b9-4bbe-86cc-b3d437ba2818”,
| “client-request-id”: “3f28d20d-67b9-4bbe-86cc-b3d437ba2818” } } }
The script still runs, but I end up with N/A in the SPO Storage Used column.
Any ideas to fix this?
Are you using V5.13? Microsoft has a problem with the usage API and SPO data https://office365itpros.com/2024/02/19/sharepoint-usage-data-issue/. I updated the script with a workaround. It’s imperfect but you should use the latest version of the script.
Thanks for a quick response! I have been using v5.12. Will try the new one now
Did not get the original error i reported in v5.13, but now getting error in PS1:331
Line |
331 | If ($GroupOwners[0].’@odata.type’ -eq ‘#microsoft.graph.user’) { …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Cannot index into a null array.
Thanks
You must have a group with no owners…
In any case, I have fixed the bug. Grab the update from GitHub.
I might as well just call you The Flash! Thanks!
Hello again!
After running for about an hour, i started getting the error below
Line |
329 | … GroupData = Invoke-RestMethod -uri $Uri -Headers $Headers -Method Get …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| { “error”: { “code”: “InvalidAuthenticationToken”, “message”: “Lifetime validation failed, the token
| is expired.”, “innerError”: { “date”: “2024-03-20T14:57:37”, “request-id”:
| “e06b1ab2-209e-4051-b889-640da432cae4”, “client-request-id”: “e06b1ab2-209e-4051-b889-640da432cae4” }
| } }
Looks like the access token expired. There’s code in the script to renew the access token automatically. I’ve moved it to the start of processing each group and decreased the time interval used for the check. Hopefully these fix the problems. Download the fixed script from GitHub.
Hello, another set of errors I have just encountered.
When i ran 5.12 with the SharePoint api error, it completed though with no details in the SPO usage, but it reported the site/group ownership accurately.
With 5.13, I am getting lots of “no owner found” on sites/groups that were previously reported accurately.
Another error with lines 533,534,and 535
Line |
533 | [datetime]$LastItemAddedToTeams = $ThisTeamData.LastActiv …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Cannot convert value “21-Sept-2023” to type “System.DateTime”. Error: “The string ’21-Sept-2023′ was not
| recognized as a valid DateTime. There is an unknown word starting at index ‘3’.”
Get-Date: C:\Users\me\Scripts\TeamsGroupsActivityReportV5.PS1:534
Line |
534 | … $LastItemAddedToTeams = Get-Date ($LastItemAddedtoTeams) -Format …
| ~~~~~~~~~~~~~~~~~~~~~~~
| Cannot bind parameter ‘Date’. Cannot convert value “” to type “System.DateTime”. Error: “String ” was not
| recognized as a valid DateTime.”
New-TimeSpan: C:\Users\me\Scripts\TeamsGroupsActivityReportV5.PS1:535
Line |
535 | … t]$DaysSinceTeamsActivity = (New-TimeSpan $LastItemAddedtoTeams).Days
| ~~~~~~~~~~~~~~~~~~~~~
| Cannot bind parameter ‘Start’. Cannot convert value “” to type “System.DateTime”. Error: “String ” was not
| recognized as a valid DateTime.”
Processed 2082 groups in 32287.74 – Currently processing at 15.51 seconds per group
The error with the date is probably due to the date/time format defined for PowerShell on your computer. See https://blog.sheehans.org/2017/09/24/powershell-datetime-throws-the-error-string-was-not-recognized-as-a-valid-datetime/. I have no idea what that is, but what’s for sure is that the string 21-Sept-2023 (which is a valid date in many places) is not considered OK by the culture used by you (visible with (Get-Culture).DateTimeFormat). You could try replacing line 533 with:
[datetime]$LastItemAddedToTeams = [datetime]::ParseExact($ThisTeamData.LastActivity, “dd-MMM-yyyy”, $null)
See if that works for you.
AS to no owner found, the code to do this is shown below. I think I tweaked this the other day because the data returned by the Graph call seemed to have changed recently. Make sure that you’re running this version.
$Uri = (“https://graph.microsoft.com/v1.0/groups/{0}/owners?`$select=id,displayName,mail” -f $Group.Id)
[array]$GroupData = Invoke-RestMethod -uri $Uri -Headers $Headers -Method Get
[array]$GroupOwners = $GroupData.Value
If ($GroupOwners) {
If ($GroupOwners[0].’@odata.type’ -eq ‘#microsoft.graph.user’) { # Found a group owner, so extract names
$OwnerNames = $GroupOwners.displayName -join “, ”
$GroupOwnerEmail = $GroupOwners[0].Mail
}
} Else { # ownerless group
$OwnerNames = “No owners found”
$GroupOwnerEMail = $null
}
Hello,
Unfortunately, v5.13 still timing out at line 332, even with the addition of line 429 $Global:Token = CheckAccessToken
Line |
332 | … GroupData = Invoke-RestMethod -uri $Uri -Headers $Headers -Method Get …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| { “error”: { “code”: “InvalidAuthenticationToken”, “message”: “Lifetime validation failed, the token
| is expired.”, “innerError”: { “date”: “2024-03-21T11:34:29”, “request-id”:
| “421bff25-1d54-431f-baf2-a1b4588f5d6b”, “client-request-id”: “421bff25-1d54-431f-baf2-a1b4588f5d6b” }
| } }
Well, the way things work is that CheckAccess should generate a new access token if the token expiration time is exceeded. The only thing that I could see is that the $TimeNow variable wasn’t defined as a [datetime], so I changed that (newer versions of PowerShell could be more insistent on casting, but I haven’t seen this problem before). Maybe this will work better:
Function CheckAccessToken {
# Function to check if the access token needs to be refreshed. If it does, request a new token
# This often needs to happen when the script processes more than a few thousands groups
[datetime]$TimeNow = (Get-Date)
If ($TimeNow -ge $TokenExpiredDate) {
$Global:Token = GetAccessToken
$Global:TokenExpiredDate = (Get-Date).AddMinutes($TimeToRefreshToken)
Write-Host (“The next time to renew the access token is {0}}” -f ($TokenExpiredDate.toString()))
}
I also reduced the time interval for token refresh from 55 to 53 minutes. See if these changes help.