@@ -668,51 +668,120 @@ def create_release(self, repo_owner: str, repo_name: str, tag_name: str, release
668668
669669 def user_activity_query (self , variables : dict [str , Any ], query : str ) -> Dict [str , Any ]:
670670 """
671- Performs a user activity query using GitHub's GraphQL API for the authenticated user (token owner).
671+ Performs a user activity query using GitHub's GraphQL API with support for organization-specific
672+ and cross-organization queries.
672673
673- **Critical**: To query activities within a specific organization (e.g., "saidsef"):
674- 1. Query the organization DIRECTLY using `organization(login: "saidsef")`
675- 2. Do NOT use `viewer.contributionsCollection` as it excludes many private org activities
676- 3. Organization name is CASE-SENSITIVE - must match exactly
677-
678- **For Organization-Specific Activity** (e.g., saidsef):
674+ **Query Modes**:
675+
676+ 1. **Organization-Specific Activity** (fastest, most comprehensive):
677+ - Query organization repositories directly
678+ - Access all private repos in the org (with proper token scopes)
679+ - Get detailed commit history, PRs, and issues
680+ - Variables: {"orgName": "Pelle-Tech", "from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"}
681+ - Variable types: `$orgName: String!`, `$from: GitTimestamp!`, `$to: GitTimestamp!`
682+
683+ 2. **Authenticated User Activity Across All Orgs** (slower, summary only):
684+ - Query viewer's contribution collection
685+ - Includes all orgs where user is a member
686+ - Summary counts only (no detailed commit messages)
687+ - Variables: {"from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"}
688+ - Variable types: `$from: DateTime!`, `$to: DateTime!`
689+
690+ 3. **User Activity in Specific Organization** (most restrictive):
691+ - Query organization repos filtered by user
692+ - Requires combining org query with author filtering
693+ - Variables: {"orgName": "Pelle-Tech", "username": "saidsef", "from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"}
694+ - Variable types: `$orgName: String!`, `$username: String!`, `$from: GitTimestamp!`, `$to: GitTimestamp!`
695+
696+ **Performance Tips**:
697+ - Use pagination parameters to limit initial data: `first: 50` instead of `first: 100`
698+ - Query only required fields to reduce response size
699+ - Use org-specific queries when possible (faster than viewer queries)
700+ - For large date ranges, split into smaller queries
701+ - Cache results for repeated queries
702+
703+ **Example Queries**:
704+
705+ **Fast Org Query with Pagination**:
679706 ```graphql
680- query($orgName: String!, $from: GitTimestamp!, $to: GitTimestamp!) {
707+ query($orgName: String!, $from: GitTimestamp!, $to: GitTimestamp!, $repoCount: Int = 50 ) {
681708 organization(login: $orgName) {
682709 login
683- repositories(first: 100, privacy: PRIVATE, orderBy: {field: PUSHED_AT, direction: DESC}) {
710+ repositories(first: $repoCount, privacy: PRIVATE, orderBy: {field: PUSHED_AT, direction: DESC}) {
711+ pageInfo {
712+ hasNextPage
713+ endCursor
714+ }
684715 nodes {
685716 name
686717 isPrivate
687- owner { login }
688718 defaultBranchRef {
689719 target {
690720 ... on Commit {
691- history(since: $from, until: $to) {
721+ history(since: $from, until: $to, first: 100 ) {
692722 totalCount
723+ pageInfo {
724+ hasNextPage
725+ endCursor
726+ }
693727 nodes {
694- author { user { login } }
728+ author {
729+ user { login }
730+ email
731+ }
695732 committedDate
696733 message
734+ additions
735+ deletions
697736 }
698737 }
699738 }
700739 }
701740 }
702- pullRequests(first: 100, states: [OPEN, CLOSED, MERGED], orderBy: {field: UPDATED_AT, direction: DESC}) {
741+ pullRequests(first: 50, states: [OPEN, CLOSED, MERGED], orderBy: {field: UPDATED_AT, direction: DESC}) {
742+ totalCount
703743 nodes {
704744 number
705745 title
706746 author { login }
707747 createdAt
708748 state
749+ additions
750+ deletions
751+ }
752+ }
753+ }
754+ }
755+ }
756+ }
757+ ```
758+
759+ **User-Filtered Org Query**:
760+ ```graphql
761+ query($orgName: String!, $username: String!, $from: GitTimestamp!, $to: GitTimestamp!) {
762+ organization(login: $orgName) {
763+ login
764+ repositories(first: 100, privacy: PRIVATE) {
765+ nodes {
766+ name
767+ defaultBranchRef {
768+ target {
769+ ... on Commit {
770+ history(since: $from, until: $to, author: {emails: [$username]}, first: 100) {
771+ totalCount
772+ nodes {
773+ author { user { login } }
774+ committedDate
775+ message
776+ }
777+ }
778+ }
709779 }
710780 }
711- issues (first: 100, states: [OPEN, CLOSED], orderBy: {field: UPDATED_AT, direction: DESC} ) {
781+ pullRequests (first: 100, states: [OPEN, CLOSED, MERGED] ) {
712782 nodes {
713- number
714- title
715783 author { login }
784+ title
716785 createdAt
717786 state
718787 }
@@ -723,64 +792,147 @@ def user_activity_query(self, variables: dict[str, Any], query: str) -> Dict[str
723792 }
724793 ```
725794
726- **For Authenticated User Activity Across All Orgs **:
795+ **Cross-Org Viewer Query **:
727796 ```graphql
728797 query($from: DateTime!, $to: DateTime!) {
729798 viewer {
730799 login
731800 contributionsCollection(from: $from, to: $to) {
732801 commitContributionsByRepository(maxRepositories: 100) {
733- repository { name isPrivate owner { login } }
802+ repository {
803+ name
804+ isPrivate
805+ owner { login }
806+ }
734807 contributions { totalCount }
735808 }
736809 pullRequestContributionsByRepository(maxRepositories: 100) {
737- repository { name isPrivate owner { login } }
810+ repository {
811+ name
812+ isPrivate
813+ owner { login }
814+ }
815+ contributions { totalCount }
816+ }
817+ issueContributionsByRepository(maxRepositories: 100) {
818+ repository {
819+ name
820+ isPrivate
821+ owner { login }
822+ }
738823 contributions { totalCount }
739824 }
740825 }
741826 organizations(first: 100) {
742- nodes { login }
827+ nodes {
828+ login
829+ viewerCanAdminister
830+ }
743831 }
744832 }
745833 }
746834 ```
747835
748836 Args:
749- variables (dict[str, Any]): Query variables. Options:
750- - For org-specific with commit history: {"orgName": "saidsef", "from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"}
751- Note: Use GitTimestamp type (ISO 8601 format) for $from/$to when querying commit history
752- - For all activity: {"from": "2024-10-01T00:00:00Z", "to": "2024-10-31T23:59:59Z"}
753- Note: Use DateTime type for contributionsCollection queries
754- query (str): GraphQL query string. Use `organization(login: $orgName)` for specific org queries.
755- IMPORTANT: Declare variables as `GitTimestamp!` for commit history, `DateTime!` for contributionsCollection.
837+ variables (dict[str, Any]): Query variables. Supported combinations:
838+ - Org-specific: {"orgName": "Pelle-Tech", "from": "...", "to": "..."}
839+ - Cross-org: {"from": "...", "to": "..."}
840+ - User-filtered org: {"orgName": "Pelle-Tech", "username": "saidsef", "from": "...", "to": "..."}
841+ - With pagination: Add {"repoCount": 50, "prCount": 50} for custom limits
842+ query (str): GraphQL query string. Must declare correct variable types:
843+ - Organization queries: Use `GitTimestamp!` for $from/$to
844+ - Viewer queries: Use `DateTime!` for $from/$to
845+ - Both types accept ISO 8601 format: "YYYY-MM-DDTHH:MM:SSZ"
756846
757847 Returns:
758- Dict[str, Any]: The JSON response from GitHub's GraphQL API containing activity data.
848+ Dict[str, Any]: GraphQL response with activity data or error information.
849+ - Success: {"data": {...}}
850+ - Errors: {"errors": [...], "data": null}
851+ - Network error: {"status": "error", "message": "..."}
759852
760- Note: Organization queries require `read:org` scope. Organization name is case-sensitive.
761- GitTimestamp and DateTime both accept ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ).
762- """
763- logging .info (f"Performing GraphQL query with variables: { variables } " )
853+ Error Handling:
854+ - Validates response status codes
855+ - Logs GraphQL errors with details
856+ - Returns structured error responses
857+ - Includes traceback for debugging
858+
859+ Required Token Scopes:
860+ - `repo`: Full control of private repositories
861+ - `read:org`: Read org and team membership
862+ - `read:user`: Read user profile data
863+
864+ Performance Notes:
865+ - Org queries are ~3x faster than viewer queries
866+ - Large date ranges (>1 year) may timeout
867+ - Use pagination for repos with >100 commits
868+ - Response size correlates with date range and repo count
869+ """
870+ # Validate inputs
871+ if not query or not isinstance (query , str ):
872+ return {"status" : "error" , "message" : "Query must be a non-empty string" }
873+
874+ if not variables or not isinstance (variables , dict ):
875+ return {"status" : "error" , "message" : "Variables must be a non-empty dictionary" }
876+
877+ # Determine query type for optimized logging
878+ query_type = "unknown"
879+ if "orgName" in variables and "username" in variables :
880+ query_type = "user-filtered-org"
881+ elif "orgName" in variables :
882+ query_type = "org-specific"
883+ elif "from" in variables and "to" in variables :
884+ query_type = "cross-org-viewer"
885+
886+ logging .info (f"Performing GraphQL query [type: { query_type } ] with variables: { variables } " )
764887
765888 try :
889+ # Make GraphQL request with optimized timeout
766890 response = requests .post (
767891 'https://api.github.com/graphql' ,
768892 json = {'query' : query , 'variables' : variables },
769893 headers = self ._get_headers (),
770- timeout = TIMEOUT
894+ timeout = TIMEOUT * 2 # Double timeout for GraphQL queries (can be complex)
771895 )
772896 response .raise_for_status ()
773897 query_data = response .json ()
774898
899+ # Handle GraphQL errors (API accepts request but query has issues)
775900 if 'errors' in query_data :
776- logging .error (f"GraphQL errors: { query_data ['errors' ]} " )
777-
901+ error_messages = [err .get ('message' , 'Unknown error' ) for err in query_data ['errors' ]]
902+ logging .error (f"GraphQL query errors: { error_messages } " )
903+
904+ # Check for common errors and provide helpful messages
905+ for error in query_data ['errors' ]:
906+ error_type = error .get ('extensions' , {}).get ('code' )
907+ if error_type == 'variableMismatch' :
908+ logging .error (f"Variable type mismatch: Use GitTimestamp for org queries, DateTime for viewer queries" )
909+ elif error_type == 'NOT_FOUND' :
910+ logging .error (f"Resource not found: Check org/user name is correct and case-sensitive" )
911+ elif error_type == 'FORBIDDEN' :
912+ logging .error (f"Access forbidden: Check token has required scopes (repo, read:org)" )
913+
914+ return query_data # Return with errors for caller to handle
915+
916+ # Log success with summary
917+ if 'data' in query_data :
918+ data_keys = list (query_data ['data' ].keys ())
919+ logging .info (f"GraphQL query successful [type: { query_type } ], returned data keys: { data_keys } " )
920+
778921 return query_data
922+
923+ except requests .exceptions .Timeout :
924+ error_msg = f"GraphQL query timeout after { TIMEOUT * 2 } s. Try reducing date range or repo count."
925+ logging .error (error_msg )
926+ return {"status" : "error" , "message" : error_msg , "timeout" : True }
927+
779928 except requests .exceptions .RequestException as req_err :
780- logging .error (f"Request error during user activity query: { str (req_err )} " )
929+ error_msg = f"Request error during GraphQL query: { str (req_err )} "
930+ logging .error (error_msg )
781931 traceback .print_exc ()
782- return {"status" : "error" , "message" : str (req_err )}
932+ return {"status" : "error" , "message" : error_msg , "request_exception" : True }
933+
783934 except Exception as e :
784- logging .error (f"Error performing user activity query: { str (e )} " )
935+ error_msg = f"Unexpected error performing GraphQL query: { str (e )} "
936+ logging .error (error_msg )
785937 traceback .print_exc ()
786- return {"status" : "error" , "message" : str ( e ) }
938+ return {"status" : "error" , "message" : error_msg , "unexpected" : True }
0 commit comments