Skip to content

Commit 3013bbb

Browse files
committed
fix(cursor): align extractor raw params and handle fractional requestsCosts
Signed-off-by: Joshua Smith <jbsmith7741@gmail.com>
1 parent 0b094e1 commit 3013bbb

19 files changed

Lines changed: 286 additions & 49 deletions
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package e2e
19+
20+
import (
21+
"testing"
22+
"time"
23+
24+
"github.com/apache/incubator-devlake/core/config"
25+
"github.com/apache/incubator-devlake/core/models/common"
26+
"github.com/apache/incubator-devlake/core/runner"
27+
"github.com/apache/incubator-devlake/helpers/e2ehelper"
28+
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
29+
"github.com/apache/incubator-devlake/plugins/cursor/impl"
30+
"github.com/apache/incubator-devlake/plugins/cursor/models"
31+
"github.com/apache/incubator-devlake/plugins/cursor/tasks"
32+
)
33+
34+
func TestCursorExtractorsDataFlow(t *testing.T) {
35+
cfg := config.GetConfig()
36+
dbUrl := cfg.GetString("E2E_DB_URL")
37+
if dbUrl == "" {
38+
t.Skip("skipping e2e test: E2E_DB_URL is not set")
39+
}
40+
if err := runner.CheckDbConnection(dbUrl, 10*time.Second); err != nil {
41+
t.Skipf("skipping e2e test: cannot connect to E2E_DB_URL: %v", err)
42+
}
43+
44+
var cursorPlugin impl.Cursor
45+
dataflowTester := e2ehelper.NewDataFlowTester(t, "cursor", cursorPlugin)
46+
47+
taskData := &tasks.CursorTaskData{
48+
Options: &tasks.CursorOptions{
49+
ConnectionId: 1,
50+
ScopeId: "team",
51+
},
52+
Connection: &models.CursorConnection{
53+
CursorConn: models.CursorConn{
54+
RestConnection: helper.RestConnection{Endpoint: models.DefaultEndpoint},
55+
},
56+
},
57+
}
58+
59+
dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_cursor_usage_events.csv", "_raw_cursor_usage_events")
60+
dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_cursor_members.csv", "_raw_cursor_members")
61+
dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_cursor_user_spend.csv", "_raw_cursor_user_spend")
62+
63+
dataflowTester.FlushTabler(&models.CursorUsageEvent{})
64+
dataflowTester.FlushTabler(&models.CursorMember{})
65+
dataflowTester.FlushTabler(&models.CursorUserSpend{})
66+
67+
dataflowTester.Subtask(tasks.ExtractMembersMeta, taskData)
68+
dataflowTester.Subtask(tasks.ExtractUsageEventsMeta, taskData)
69+
dataflowTester.Subtask(tasks.ExtractUserSpendMeta, taskData)
70+
71+
dataflowTester.VerifyTableWithOptions(&models.CursorUsageEvent{}, e2ehelper.TableOptions{
72+
CSVRelPath: "./snapshot_tables/_tool_cursor_usage_events.csv",
73+
IgnoreTypes: []interface{}{common.NoPKModel{}},
74+
})
75+
76+
dataflowTester.VerifyTableWithOptions(&models.CursorMember{}, e2ehelper.TableOptions{
77+
CSVRelPath: "./snapshot_tables/_tool_cursor_members.csv",
78+
IgnoreTypes: []interface{}{common.NoPKModel{}},
79+
})
80+
81+
dataflowTester.VerifyTableWithOptions(&models.CursorUserSpend{}, e2ehelper.TableOptions{
82+
CSVRelPath: "./snapshot_tables/_tool_cursor_user_spend.csv",
83+
IgnoreTypes: []interface{}{common.NoPKModel{}},
84+
})
85+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
id,params,data,url,input,created_at
2+
1,"{""ConnectionId"":1,""ScopeId"":""team"",""Endpoint"":""https://api.cursor.com""}","{""name"":""Example User"",""email"":""user@example.com"",""id"":""user_example123"",""role"":""member"",""isRemoved"":false}",https://api.cursor.com/teams/members,null,2026-07-09 17:14:19.000
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
id,params,data,url,input,created_at
2+
1,"{""ConnectionId"":1,""ScopeId"":""team"",""Endpoint"":""https://api.cursor.com""}","{""timestamp"":""1783617259310"",""model"":""composer-2.5-fast"",""kind"":""Usage-based"",""maxMode"":false,""requestsCosts"":2,""isTokenBasedCall"":false,""tokenUsage"":{""inputTokens"":5932,""outputTokens"":4761,""cacheWriteTokens"":0,""cacheReadTokens"":495701,""totalCents"":33.70615},""userEmail"":""user@example.com"",""isChargeable"":true,""serviceAccountId"":""null"",""isHeadless"":false,""chargedCents"":8,""conversationId"":""846952e0-0221-473b-bb1d-e5d016f071b1""}",https://api.cursor.com/teams/filtered-usage-events,null,2026-07-09 17:14:19.000
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
id,params,data,url,input,created_at
2+
1,"{""ConnectionId"":1,""ScopeId"":""team"",""Endpoint"":""https://api.cursor.com""}","{""subscriptionCycleStart"":1781472188000,""collectedAt"":""2026-07-10T17:31:24.141244Z"",""member"":{""userId"":""user_example123"",""spendCents"":3224,""includedSpendCents"":1984,""fastPremiumRequests"":496,""name"":""Example User"",""email"":""user@example.com"",""profilePictureUrl"":null,""role"":""member"",""monthlyLimitDollars"":null,""hardLimitOverrideDollars"":0}}",https://api.cursor.com/teams/spend,null,2026-07-10 17:31:24.000
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
connection_id,scope_id,user_id,email,name,role,is_removed
2+
1,team,user_example123,user@example.com,Example User,member,0
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
connection_id,scope_id,event_id,event_time,user_email,model,kind,conversation_id,charged_cents,requests_costs,is_token_based_call,is_chargeable,max_mode,is_headless,input_tokens,output_tokens,cache_read_tokens,cache_write_tokens,total_cents,cursor_token_fee,hosting_type,service_account_id
2+
1,team,2fe8f95cb983e68c7e944264bf1cc976c4940e1d3045f8fd5715fb66466d6eb7,2026-07-09T17:14:19.310+00:00,user@example.com,composer-2.5-fast,Usage-based,846952e0-0221-473b-bb1d-e5d016f071b1,8,2,0,1,0,0,5932,4761,495701,0,33.70615,0,,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
connection_id,scope_id,user_id,billing_cycle_start,collected_at,email,name,role,spend_cents,included_spend_cents,fast_premium_requests,monthly_limit_dollars,hard_limit_override_dollars
2+
1,team,user_example123,2026-06-14T21:23:08.000+00:00,2026-07-10T17:31:24.141+00:00,user@example.com,Example User,member,3224,1984,496,0,0
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package migrationscripts
19+
20+
import (
21+
"github.com/apache/incubator-devlake/core/context"
22+
"github.com/apache/incubator-devlake/core/errors"
23+
)
24+
25+
type changeCursorRequestsCostsToFloat struct{}
26+
27+
func (script *changeCursorRequestsCostsToFloat) Up(basicRes context.BasicRes) errors.Error {
28+
db := basicRes.GetDal()
29+
if !db.HasTable("_tool_cursor_usage_events") {
30+
return nil
31+
}
32+
return db.Exec("ALTER TABLE _tool_cursor_usage_events MODIFY COLUMN requests_costs DOUBLE")
33+
}
34+
35+
func (*changeCursorRequestsCostsToFloat) Version() uint64 { return 20260710120000 }
36+
37+
func (*changeCursorRequestsCostsToFloat) Name() string {
38+
return "cursor change requests_costs column to double"
39+
}

backend/plugins/cursor/models/migrationscripts/register.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ import "github.com/apache/incubator-devlake/core/plugin"
2323
func All() []plugin.MigrationScript {
2424
return []plugin.MigrationScript{
2525
new(addCursorInitialTables),
26+
new(changeCursorRequestsCostsToFloat),
2627
}
2728
}

backend/plugins/cursor/models/usage_event.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ type CursorUsageEvent struct {
3535
Kind string `json:"kind" gorm:"type:varchar(100)"`
3636
ConversationId string `json:"conversationId" gorm:"type:varchar(64);index"`
3737
ChargedCents float64 `json:"chargedCents"`
38-
RequestsCosts int `json:"requestsCosts"`
38+
RequestsCosts float64 `json:"requestsCosts"`
3939
IsTokenBasedCall bool `json:"isTokenBasedCall"`
4040
IsChargeable bool `json:"isChargeable"`
4141
MaxMode bool `json:"maxMode"`

0 commit comments

Comments
 (0)