@@ -28,6 +28,8 @@ import (
2828 "github.com/apache/incubator-devlake/plugins/claude_code/models"
2929)
3030
31+ // ── Enterprise API response types (/v1/organizations/analytics/users) ──────────
32+
3133// userActivityRecord is the JSON shape returned by /v1/organizations/analytics/users.
3234type userActivityRecord struct {
3335 User userActivityUser `json:"user"`
@@ -82,6 +84,48 @@ type userActivityCCTools struct {
8284 NotebookEditTool userActivityToolAction `json:"notebook_edit_tool"`
8385}
8486
87+ // ── Console API response types (/v1/organizations/usage_report/claude_code) ───
88+
89+ // consoleUserActivityRecord is the JSON shape returned by /v1/organizations/usage_report/claude_code.
90+ type consoleUserActivityRecord struct {
91+ Date string `json:"date"`
92+ Actor consoleActor `json:"actor"`
93+ CoreMetrics consoleCoreMetrics `json:"core_metrics"`
94+ ToolActions consoleToolActions `json:"tool_actions"`
95+ }
96+
97+ type consoleActor struct {
98+ Type string `json:"type"`
99+ EmailAddress string `json:"email_address"`
100+ ApiKeyName string `json:"api_key_name"`
101+ }
102+
103+ type consoleCoreMetrics struct {
104+ NumSessions int `json:"num_sessions"`
105+ LinesOfCode consoleLinesOfCode `json:"lines_of_code"`
106+ CommitsByClaudeCode int `json:"commits_by_claude_code"`
107+ PullRequestsByClaudeCode int `json:"pull_requests_by_claude_code"`
108+ }
109+
110+ type consoleLinesOfCode struct {
111+ Added int `json:"added"`
112+ Removed int `json:"removed"`
113+ }
114+
115+ type consoleToolAction struct {
116+ Accepted int `json:"accepted"`
117+ Rejected int `json:"rejected"`
118+ }
119+
120+ type consoleToolActions struct {
121+ EditTool consoleToolAction `json:"edit_tool"`
122+ MultiEditTool consoleToolAction `json:"multi_edit_tool"`
123+ WriteTool consoleToolAction `json:"write_tool"`
124+ NotebookEditTool consoleToolAction `json:"notebook_edit_tool"`
125+ }
126+
127+ // ── Extractor ─────────────────────────────────────────────────────────────────
128+
85129// ExtractUserActivity parses raw user activity records into tool-layer tables.
86130func ExtractUserActivity (taskCtx plugin.SubTaskContext ) errors.Error {
87131 data , ok := taskCtx .TaskContext ().GetData ().(* ClaudeCodeTaskData )
@@ -96,6 +140,12 @@ func ExtractUserActivity(taskCtx plugin.SubTaskContext) errors.Error {
96140 return nil
97141 }
98142
143+ isConsole := connection .IsConsoleApiKey ()
144+ endpoint := "analytics/users"
145+ if isConsole {
146+ endpoint = "usage_report/claude_code"
147+ }
148+
99149 extractor , err := helper .NewApiExtractor (helper.ApiExtractorArgs {
100150 RawDataSubTaskArgs : helper.RawDataSubTaskArgs {
101151 Ctx : taskCtx ,
@@ -104,63 +154,18 @@ func ExtractUserActivity(taskCtx plugin.SubTaskContext) errors.Error {
104154 ConnectionId : data .Options .ConnectionId ,
105155 ScopeId : data .Options .ScopeId ,
106156 Organization : connection .Organization ,
107- Endpoint : "analytics/users" ,
157+ Endpoint : endpoint ,
108158 },
109159 },
110160 Extract : func (row * helper.RawData ) ([]interface {}, errors.Error ) {
111- var record userActivityRecord
112- if err := errors .Convert (json .Unmarshal (row .Data , & record )); err != nil {
113- return nil , err
114- }
115-
116161 date , parseErr := parseAnalyticsDate (row .Input )
117162 if parseErr != nil {
118163 return nil , parseErr
119164 }
120-
121- userId := strings .TrimSpace (record .User .Id )
122- if userId == "" {
123- userId = strings .TrimSpace (record .User .EmailAddress )
124- }
125- if userId == "" {
126- return nil , nil
127- }
128-
129- activity := & models.ClaudeCodeUserActivity {
130- ConnectionId : data .Options .ConnectionId ,
131- ScopeId : data .Options .ScopeId ,
132- Date : date ,
133- UserId : userId ,
134- UserEmail : strings .TrimSpace (record .User .EmailAddress ),
135-
136- ChatConversationCount : record .ChatMetrics .DistinctConversationCount ,
137- ChatMessageCount : record .ChatMetrics .MessageCount ,
138- ChatProjectsCreatedCount : record .ChatMetrics .DistinctProjectsCreatedCount ,
139- ChatProjectsUsedCount : record .ChatMetrics .DistinctProjectsUsedCount ,
140- ChatFilesUploadedCount : record .ChatMetrics .DistinctFilesUploadedCount ,
141- ChatArtifactsCreatedCount : record .ChatMetrics .DistinctArtifactsCreatedCount ,
142- ChatThinkingMessageCount : record .ChatMetrics .ThinkingMessageCount ,
143- ChatSkillsUsedCount : record .ChatMetrics .DistinctSkillsUsedCount ,
144- ChatConnectorsUsedCount : record .ChatMetrics .ConnectorsUsedCount ,
145-
146- CCCommitCount : record .ClaudeCodeMetrics .CoreMetrics .CommitCount ,
147- CCPullRequestCount : record .ClaudeCodeMetrics .CoreMetrics .PullRequestCount ,
148- CCLinesAdded : record .ClaudeCodeMetrics .CoreMetrics .LinesOfCode .AddedCount ,
149- CCLinesRemoved : record .ClaudeCodeMetrics .CoreMetrics .LinesOfCode .RemovedCount ,
150- CCSessionCount : record .ClaudeCodeMetrics .CoreMetrics .DistinctSessionCount ,
151-
152- EditToolAccepted : record .ClaudeCodeMetrics .ToolActions .EditTool .AcceptedCount ,
153- EditToolRejected : record .ClaudeCodeMetrics .ToolActions .EditTool .RejectedCount ,
154- MultiEditToolAccepted : record .ClaudeCodeMetrics .ToolActions .MultiEditTool .AcceptedCount ,
155- MultiEditToolRejected : record .ClaudeCodeMetrics .ToolActions .MultiEditTool .RejectedCount ,
156- WriteToolAccepted : record .ClaudeCodeMetrics .ToolActions .WriteTool .AcceptedCount ,
157- WriteToolRejected : record .ClaudeCodeMetrics .ToolActions .WriteTool .RejectedCount ,
158- NotebookEditToolAccepted : record .ClaudeCodeMetrics .ToolActions .NotebookEditTool .AcceptedCount ,
159- NotebookEditToolRejected : record .ClaudeCodeMetrics .ToolActions .NotebookEditTool .RejectedCount ,
160-
161- WebSearchCount : record .WebSearchCount ,
165+ if isConsole {
166+ return extractConsoleUserActivity (data , date , row .Data )
162167 }
163- return [] interface {}{ activity }, nil
168+ return extractEnterpriseUserActivity ( data , date , row . Data )
164169 },
165170 })
166171 if err != nil {
@@ -169,18 +174,106 @@ func ExtractUserActivity(taskCtx plugin.SubTaskContext) errors.Error {
169174 return extractor .Execute ()
170175}
171176
177+ func extractEnterpriseUserActivity (data * ClaudeCodeTaskData , date time.Time , raw []byte ) ([]interface {}, errors.Error ) {
178+ var record userActivityRecord
179+ if err := errors .Convert (json .Unmarshal (raw , & record )); err != nil {
180+ return nil , err
181+ }
182+
183+ userId := strings .TrimSpace (record .User .Id )
184+ if userId == "" {
185+ userId = strings .TrimSpace (record .User .EmailAddress )
186+ }
187+ if userId == "" {
188+ return nil , nil
189+ }
190+
191+ activity := & models.ClaudeCodeUserActivity {
192+ ConnectionId : data .Options .ConnectionId ,
193+ ScopeId : data .Options .ScopeId ,
194+ Date : date ,
195+ UserId : userId ,
196+ UserEmail : strings .TrimSpace (record .User .EmailAddress ),
197+
198+ ChatConversationCount : record .ChatMetrics .DistinctConversationCount ,
199+ ChatMessageCount : record .ChatMetrics .MessageCount ,
200+ ChatProjectsCreatedCount : record .ChatMetrics .DistinctProjectsCreatedCount ,
201+ ChatProjectsUsedCount : record .ChatMetrics .DistinctProjectsUsedCount ,
202+ ChatFilesUploadedCount : record .ChatMetrics .DistinctFilesUploadedCount ,
203+ ChatArtifactsCreatedCount : record .ChatMetrics .DistinctArtifactsCreatedCount ,
204+ ChatThinkingMessageCount : record .ChatMetrics .ThinkingMessageCount ,
205+ ChatSkillsUsedCount : record .ChatMetrics .DistinctSkillsUsedCount ,
206+ ChatConnectorsUsedCount : record .ChatMetrics .ConnectorsUsedCount ,
207+
208+ CCCommitCount : record .ClaudeCodeMetrics .CoreMetrics .CommitCount ,
209+ CCPullRequestCount : record .ClaudeCodeMetrics .CoreMetrics .PullRequestCount ,
210+ CCLinesAdded : record .ClaudeCodeMetrics .CoreMetrics .LinesOfCode .AddedCount ,
211+ CCLinesRemoved : record .ClaudeCodeMetrics .CoreMetrics .LinesOfCode .RemovedCount ,
212+ CCSessionCount : record .ClaudeCodeMetrics .CoreMetrics .DistinctSessionCount ,
213+
214+ EditToolAccepted : record .ClaudeCodeMetrics .ToolActions .EditTool .AcceptedCount ,
215+ EditToolRejected : record .ClaudeCodeMetrics .ToolActions .EditTool .RejectedCount ,
216+ MultiEditToolAccepted : record .ClaudeCodeMetrics .ToolActions .MultiEditTool .AcceptedCount ,
217+ MultiEditToolRejected : record .ClaudeCodeMetrics .ToolActions .MultiEditTool .RejectedCount ,
218+ WriteToolAccepted : record .ClaudeCodeMetrics .ToolActions .WriteTool .AcceptedCount ,
219+ WriteToolRejected : record .ClaudeCodeMetrics .ToolActions .WriteTool .RejectedCount ,
220+ NotebookEditToolAccepted : record .ClaudeCodeMetrics .ToolActions .NotebookEditTool .AcceptedCount ,
221+ NotebookEditToolRejected : record .ClaudeCodeMetrics .ToolActions .NotebookEditTool .RejectedCount ,
222+
223+ WebSearchCount : record .WebSearchCount ,
224+ }
225+ return []interface {}{activity }, nil
226+ }
227+
228+ func extractConsoleUserActivity (data * ClaudeCodeTaskData , date time.Time , raw []byte ) ([]interface {}, errors.Error ) {
229+ var record consoleUserActivityRecord
230+ if err := errors .Convert (json .Unmarshal (raw , & record )); err != nil {
231+ return nil , err
232+ }
233+
234+ userId := strings .TrimSpace (record .Actor .EmailAddress )
235+ if userId == "" {
236+ userId = strings .TrimSpace (record .Actor .ApiKeyName )
237+ }
238+ if userId == "" {
239+ return nil , nil
240+ }
241+
242+ activity := & models.ClaudeCodeUserActivity {
243+ ConnectionId : data .Options .ConnectionId ,
244+ ScopeId : data .Options .ScopeId ,
245+ Date : date ,
246+ UserId : userId ,
247+ UserEmail : strings .TrimSpace (record .Actor .EmailAddress ),
248+
249+ CCSessionCount : record .CoreMetrics .NumSessions ,
250+ CCCommitCount : record .CoreMetrics .CommitsByClaudeCode ,
251+ CCPullRequestCount : record .CoreMetrics .PullRequestsByClaudeCode ,
252+ CCLinesAdded : record .CoreMetrics .LinesOfCode .Added ,
253+ CCLinesRemoved : record .CoreMetrics .LinesOfCode .Removed ,
254+
255+ EditToolAccepted : record .ToolActions .EditTool .Accepted ,
256+ EditToolRejected : record .ToolActions .EditTool .Rejected ,
257+ MultiEditToolAccepted : record .ToolActions .MultiEditTool .Accepted ,
258+ MultiEditToolRejected : record .ToolActions .MultiEditTool .Rejected ,
259+ WriteToolAccepted : record .ToolActions .WriteTool .Accepted ,
260+ WriteToolRejected : record .ToolActions .WriteTool .Rejected ,
261+ NotebookEditToolAccepted : record .ToolActions .NotebookEditTool .Accepted ,
262+ NotebookEditToolRejected : record .ToolActions .NotebookEditTool .Rejected ,
263+ }
264+ return []interface {}{activity }, nil
265+ }
266+
172267// parseAnalyticsDate extracts the date from the raw row input JSON.
173268// The input is the claudeCodeDayInput or claudeCodeDateRangeInput encoded as JSON.
174269func parseAnalyticsDate (rawInput json.RawMessage ) (time.Time , errors.Error ) {
175- // Try day input first.
176270 var dayInput claudeCodeDayInput
177271 if err := json .Unmarshal (rawInput , & dayInput ); err == nil && dayInput .Day != "" {
178272 t , parseErr := time .Parse ("2006-01-02" , strings .TrimSpace (dayInput .Day ))
179273 if parseErr == nil {
180274 return utcDate (t ), nil
181275 }
182276 }
183- // Fall back to date range input (summaries).
184277 var rangeInput claudeCodeDateRangeInput
185278 if err := json .Unmarshal (rawInput , & rangeInput ); err == nil && rangeInput .StartDate != "" {
186279 t , parseErr := time .Parse ("2006-01-02" , strings .TrimSpace (rangeInput .StartDate ))
0 commit comments