Skip to content

Commit f7dd1d1

Browse files
authored
Merge pull request #1106 from bruin-data/implement-docebo-source
add docebo
2 parents a6eb919 + 73b8a8b commit f7dd1d1

File tree

11 files changed

+285
-1
lines changed

11 files changed

+285
-1
lines changed

docs/.vitepress/config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ export default withMermaid({
197197
{text: "Chess", link: "/ingestion/chess.md"},
198198
{text: "ClickUp", link: "/ingestion/clickup.md"},
199199
{text: "DB2", link: "/ingestion/db2.md"},
200+
{text: "Docebo", link: "/ingestion/docebo"},
200201
{text: "DynamoDB", link: "/ingestion/dynamodb.md"},
201202
{text: "Elasticsearch", link: "/ingestion/elasticsearch.md"},
202203
{text: "Facebook", link: "/ingestion/facebook-ads.md"},

docs/ingestion/docebo.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Docebo
2+
3+
[Docebo](https://www.docebo.com/) is a cloud-based learning management system (LMS) that helps organizations deliver, track, and manage their training programs.
4+
5+
Bruin supports Docebo as a source for [Ingestr assets](/assets/ingestr), enabling you to ingest data from your Docebo platform into your data warehouse.
6+
7+
## Connection
8+
9+
To configure a Docebo connection, you need:
10+
- **Base URL**: Your Docebo instance URL (e.g., `https://yourcompany.docebosaas.com`)
11+
- **Client ID**: OAuth2 client ID from your Docebo OAuth application
12+
- **Client Secret**: OAuth2 client secret from your Docebo OAuth application
13+
- **Username**: Your Docebo username
14+
- **Password**: Your Docebo password
15+
16+
### Getting OAuth Credentials
17+
18+
1. Log in to your Docebo platform as a Super Admin
19+
2. Navigate to **Settings****API and SSO**
20+
3. Click on **Manage** to access the API and SSO settings
21+
4. Go to the **API Credentials** tab
22+
5. Create a new OAuth2 application:
23+
- Click **Add OAuth2 App**
24+
- Enter a name for your application
25+
- Configure the appropriate scopes for your data needs
26+
- Save the application
27+
6. Note down the **Client ID** and **Client Secret**
28+
29+
## Configuration
30+
31+
Add the connection to `.bruin.yml`:
32+
33+
```yaml
34+
connections:
35+
docebo:
36+
- name: "my-docebo"
37+
base_url: "https://yourcompany.docebosaas.com"
38+
client_id: "your_client_id"
39+
client_secret: "your_client_secret"
40+
username: "your_username"
41+
password: "your_password"
42+
```
43+
44+
## Ingestr Assets
45+
46+
Once you've configured the connection, you can create an Ingestr asset to ingest data from Docebo.
47+
48+
Here's an example asset configuration:
49+
50+
```yaml
51+
name: docebo.users
52+
type: ingestr
53+
54+
parameters:
55+
source_connection: my-docebo
56+
source_table: 'users'
57+
destination: bigquery
58+
```
59+
60+
## Available Source Tables
61+
62+
| Table | Inc Strategy | Details |
63+
| ----- | ------------ | ------- |
64+
| `branches` | replace | Organization branches and departments structure. |
65+
| `categories` | replace | Course categories for organizing learning content. |
66+
| `certifications` | replace | Certification records and achievement data. |
67+
| `course_enrollments` | replace | Course enrollment data including user progress. |
68+
| `course_fields` | replace | Custom fields defined for courses. |
69+
| `course_learning_objects` | replace | Learning objects and materials within courses. |
70+
| `courses` | replace | Course information including metadata and settings. |
71+
| `external_training` | replace | External training records tracked in the system. |
72+
| `group_members` | replace | User group membership assignments. |
73+
| `groups` | replace | User groups for organizing learners. |
74+
| `learning_plan_course_enrollments` | replace | Course enrollments within learning plans. |
75+
| `learning_plan_enrollments` | replace | User enrollments in learning plans. |
76+
| `learning_plans` | replace | Learning plan definitions and structure. |
77+
| `sessions` | replace | Training session information and schedules. |
78+
| `user_fields` | replace | Custom fields defined for user profiles. |
79+
| `users` | replace | User profiles and account information. |
80+
81+
## Example: Ingesting User Data
82+
83+
Create a file `assets/docebo_users.asset.yml`:
84+
85+
```yaml
86+
name: docebo.users
87+
type: ingestr
88+
89+
parameters:
90+
source_connection: my-docebo
91+
source_table: 'users'
92+
destination: bigquery
93+
94+
columns:
95+
- name: user_id
96+
type: integer
97+
description: "Unique user identifier"
98+
- name: username
99+
type: string
100+
description: "User's username"
101+
- name: email
102+
type: string
103+
description: "User's email address"
104+
- name: first_name
105+
type: string
106+
description: "User's first name"
107+
- name: last_name
108+
type: string
109+
description: "User's last name"
110+
- name: created_at
111+
type: timestamp
112+
description: "User creation timestamp"
113+
- name: last_login
114+
type: timestamp
115+
description: "Last login timestamp"
116+
```
117+
118+
## Notes
119+
120+
- Docebo integration currently supports **full refresh** only - incremental loading is not available
121+
- The integration uses OAuth 2.0 password grant type for authentication
122+
- Invalid date fields in the source data are normalized to Unix epoch (1970-01-01)
123+
- Ensure your OAuth application has the necessary scopes to access the data you want to ingest
124+
125+
## Troubleshooting
126+
127+
### Authentication Issues
128+
- Verify that your base URL is correct and includes the protocol (https://)
129+
- Ensure your OAuth client has the correct permissions
130+
- Check that your username and password are valid
131+
132+
### Data Access Issues
133+
- Verify that your OAuth application has the necessary scopes
134+
- Check that the user account has permissions to access the requested data
135+
- Ensure the table name is spelled correctly (case-sensitive)
136+
137+
For more information on Ingestr assets, see the [Ingestr documentation](/assets/ingestr).

integration-tests/expected_connections_schema.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,12 @@
511511
},
512512
"type": "array"
513513
},
514+
"docebo": {
515+
"items": {
516+
"$ref": "#/$defs/DoceboConnection"
517+
},
518+
"type": "array"
519+
},
514520
"googleads": {
515521
"items": {
516522
"$ref": "#/$defs/GoogleAdsConnection"
@@ -846,6 +852,38 @@
846852
"region"
847853
]
848854
},
855+
"DoceboConnection": {
856+
"properties": {
857+
"name": {
858+
"type": "string"
859+
},
860+
"base_url": {
861+
"type": "string"
862+
},
863+
"client_id": {
864+
"type": "string"
865+
},
866+
"client_secret": {
867+
"type": "string"
868+
},
869+
"username": {
870+
"type": "string"
871+
},
872+
"password": {
873+
"type": "string"
874+
}
875+
},
876+
"additionalProperties": false,
877+
"type": "object",
878+
"required": [
879+
"name",
880+
"base_url",
881+
"client_id",
882+
"client_secret",
883+
"username",
884+
"password"
885+
]
886+
},
849887
"EMRServerlessConnection": {
850888
"properties": {
851889
"name": {

pkg/config/connections.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,19 @@ func (c DynamoDBConnection) GetName() string {
561561
return c.Name
562562
}
563563

564+
type DoceboConnection struct {
565+
Name string `yaml:"name,omitempty" json:"name" mapstructure:"name"`
566+
BaseURL string `yaml:"base_url,omitempty" json:"base_url" mapstructure:"base_url"`
567+
ClientID string `yaml:"client_id,omitempty" json:"client_id" mapstructure:"client_id"`
568+
ClientSecret string `yaml:"client_secret,omitempty" json:"client_secret" mapstructure:"client_secret"`
569+
Username string `yaml:"username,omitempty" json:"username" mapstructure:"username"`
570+
Password string `yaml:"password,omitempty" json:"password" mapstructure:"password"`
571+
}
572+
573+
func (c DoceboConnection) GetName() string {
574+
return c.Name
575+
}
576+
564577
type GoogleAdsConnection struct {
565578
Name string `yaml:"name,omitempty" json:"name" mapstructure:"name"`
566579
CustomerID string `yaml:"customer_id,omitempty" json:"customer_id" mapstructure:"customer_id"`

pkg/config/manager.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type Connections struct {
5959
Slack []SlackConnection `yaml:"slack,omitempty" json:"slack,omitempty" mapstructure:"slack"`
6060
Asana []AsanaConnection `yaml:"asana,omitempty" json:"asana,omitempty" mapstructure:"asana"`
6161
DynamoDB []DynamoDBConnection `yaml:"dynamodb,omitempty" json:"dynamodb,omitempty" mapstructure:"dynamodb"`
62+
Docebo []DoceboConnection `yaml:"docebo,omitempty" json:"docebo,omitempty" mapstructure:"docebo"`
6263
GoogleAds []GoogleAdsConnection `yaml:"googleads,omitempty" json:"googleads,omitempty" mapstructure:"googleads"`
6364
AppStore []AppStoreConnection `yaml:"appstore,omitempty" json:"appstore,omitempty" mapstructure:"appstore"`
6465
LinkedInAds []LinkedInAdsConnection `yaml:"linkedinads,omitempty" json:"linkedinads,omitempty" mapstructure:"linkedinads"`
@@ -661,7 +662,13 @@ func (c *Config) AddConnection(environmentName, name, connType string, creds map
661662
}
662663
conn.Name = name
663664
env.Connections.DynamoDB = append(env.Connections.DynamoDB, conn)
664-
665+
case "docebo":
666+
var conn DoceboConnection
667+
if err := mapstructure.Decode(creds, &conn); err != nil {
668+
return fmt.Errorf("failed to decode credentials: %w", err)
669+
}
670+
conn.Name = name
671+
env.Connections.Docebo = append(env.Connections.Docebo, conn)
665672
case "googleads":
666673
var conn GoogleAdsConnection
667674
if err := mapstructure.Decode(creds, &conn); err != nil {
@@ -1032,6 +1039,8 @@ func (c *Config) DeleteConnection(environmentName, connectionName string) error
10321039
env.Connections.Asana = removeConnection(env.Connections.Asana, connectionName)
10331040
case "dynamodb":
10341041
env.Connections.DynamoDB = removeConnection(env.Connections.DynamoDB, connectionName)
1042+
case "docebo":
1043+
env.Connections.Docebo = removeConnection(env.Connections.Docebo, connectionName)
10351044
case "googleads":
10361045
env.Connections.GoogleAds = removeConnection(env.Connections.GoogleAds, connectionName)
10371046
case "tiktokads":

pkg/config/manager_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,16 @@ func TestLoadFromFile(t *testing.T) {
307307
Region: "ap-south-1",
308308
},
309309
},
310+
Docebo: []DoceboConnection{
311+
{
312+
Name: "docebo-test",
313+
BaseURL: "https://mycompany.docebosaas.com",
314+
ClientID: "test-client-id",
315+
ClientSecret: "test-client-secret",
316+
Username: "admin",
317+
Password: "admin-password",
318+
},
319+
},
310320
Zendesk: []ZendeskConnection{
311321
{
312322
Name: "conn25",

pkg/config/testdata/simple.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,13 @@ environments:
181181
access_key_id: "access-key-786"
182182
secret_access_key: "shhh,secret"
183183
region: "ap-south-1"
184+
docebo:
185+
- name: "docebo-test"
186+
base_url: "https://mycompany.docebosaas.com"
187+
client_id: "test-client-id"
188+
client_secret: "test-client-secret"
189+
username: "admin"
190+
password: "admin-password"
184191
googleads:
185192
- name: googleads-0
186193
dev_token: dev-0

pkg/config/testdata/simple_win.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ environments:
163163
access_key_id: "access-key-786"
164164
secret_access_key: "shhh,secret"
165165
region: "ap-south-1"
166+
docebo:
167+
- name: "docebo-test"
168+
base_url: "https://mycompany.docebosaas.com"
169+
client_id: "test-client-id"
170+
client_secret: "test-client-secret"
171+
username: "admin"
172+
password: "admin-password"
166173
zendesk:
167174
- name: conn25
168175
api_token: "zendeskKey"

pkg/connection/connection.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/bruin-data/bruin/pkg/config"
2525
"github.com/bruin-data/bruin/pkg/databricks"
2626
"github.com/bruin-data/bruin/pkg/db2"
27+
"github.com/bruin-data/bruin/pkg/docebo"
2728
duck "github.com/bruin-data/bruin/pkg/duckdb"
2829
"github.com/bruin-data/bruin/pkg/dynamodb"
2930
"github.com/bruin-data/bruin/pkg/elasticsearch"
@@ -111,6 +112,7 @@ type Manager struct {
111112
Slack map[string]*slack.Client
112113
Asana map[string]*asana.Client
113114
DynamoDB map[string]*dynamodb.Client
115+
Docebo map[string]*docebo.Client
114116
Zendesk map[string]*zendesk.Client
115117
GoogleAds map[string]*googleads.Client
116118
TikTokAds map[string]*tiktokads.Client
@@ -1151,6 +1153,32 @@ func (m *Manager) AddDynamoDBConnectionFromConfig(connection *config.DynamoDBCon
11511153
return nil
11521154
}
11531155

1156+
func (m *Manager) AddDoceboConnectionFromConfig(connection *config.DoceboConnection) error {
1157+
m.mutex.Lock()
1158+
if m.Docebo == nil {
1159+
m.Docebo = make(map[string]*docebo.Client)
1160+
}
1161+
m.mutex.Unlock()
1162+
1163+
client, err := docebo.NewClient(docebo.Config{
1164+
BaseURL: connection.BaseURL,
1165+
ClientID: connection.ClientID,
1166+
ClientSecret: connection.ClientSecret,
1167+
Username: connection.Username,
1168+
Password: connection.Password,
1169+
})
1170+
if err != nil {
1171+
return err
1172+
}
1173+
1174+
m.mutex.Lock()
1175+
defer m.mutex.Unlock()
1176+
m.Docebo[connection.Name] = client
1177+
m.availableConnections[connection.Name] = client
1178+
m.AllConnectionDetails[connection.Name] = connection
1179+
return nil
1180+
}
1181+
11541182
func (m *Manager) AddGoogleAdsConnectionFromConfig(connection *config.GoogleAdsConnection) error {
11551183
m.mutex.Lock()
11561184
defer m.mutex.Unlock()
@@ -2084,6 +2112,7 @@ func NewManagerFromConfig(cm *config.Config) (config.ConnectionAndDetailsGetter,
20842112
processConnections(cm.SelectedEnvironment.Connections.Slack, connectionManager.AddSlackConnectionFromConfig, &wg, &errList, &mu)
20852113
processConnections(cm.SelectedEnvironment.Connections.Asana, connectionManager.AddAsanaConnectionFromConfig, &wg, &errList, &mu)
20862114
processConnections(cm.SelectedEnvironment.Connections.DynamoDB, connectionManager.AddDynamoDBConnectionFromConfig, &wg, &errList, &mu)
2115+
processConnections(cm.SelectedEnvironment.Connections.Docebo, connectionManager.AddDoceboConnectionFromConfig, &wg, &errList, &mu)
20872116
processConnections(cm.SelectedEnvironment.Connections.Zendesk, connectionManager.AddZendeskConnectionFromConfig, &wg, &errList, &mu)
20882117
processConnections(cm.SelectedEnvironment.Connections.GoogleAds, connectionManager.AddGoogleAdsConnectionFromConfig, &wg, &errList, &mu)
20892118
processConnections(cm.SelectedEnvironment.Connections.TikTokAds, connectionManager.AddTikTokAdsConnectionFromConfig, &wg, &errList, &mu)

pkg/docebo/client.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package docebo
2+
3+
// Client provides access to Docebo via ingestr.
4+
type Client struct {
5+
config Config
6+
}
7+
8+
// NewClient initializes a Docebo client with given configuration.
9+
func NewClient(c Config) (*Client, error) {
10+
return &Client{config: c}, nil
11+
}
12+
13+
// GetIngestrURI returns the ingestr URI for the client.
14+
func (c *Client) GetIngestrURI() (string, error) {
15+
return c.config.GetIngestrURI(), nil
16+
}

0 commit comments

Comments
 (0)