Skip to content

Commit

Permalink
support anonymous user
Browse files Browse the repository at this point in the history
  • Loading branch information
zensh committed Jul 7, 2020
1 parent e44f982 commit 44a0f02
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 97 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file starting fro
This project adheres to [Semantic Versioning](http://semver.org/).

-----
## [1.6.0] - 2020-07-07

**Change:**
- `GET /users/:uid/labels:cache` and `GET /v1/users/:uid/settings:unionAll` support anonymous user, uid should with prefix `anon-`.
- `GET /v1/users/:uid/settings` and `GET /v1/groups/:uid/settings` support `channel` and `client` query.

## [1.5.0] - 2020-06-08

**Change:**
Expand Down
168 changes: 86 additions & 82 deletions doc/openapi.md

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions doc/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1499,7 +1499,7 @@ paths:
get:
tags:
- User
summary: 该接口为灰度网关提供用户的灰度信息,用于服务端灰度。获取指定 uid 用户在指定 product 产品下的所有(未分页,最多 400 条)环境标签,包括从 group 群组继承的环境标签,按照 label 指派时间反序。网关只会取匹配 client 和 channel 的第一条。标签列表不是实时数据,会被服务缓存,缓存时间在 config.cache_label_expire 配置,默认为 1 分钟,建议生产配置为 5 分钟。当 uid 对应用户不存在或 product 对应产品不存在时,该接口会返回空环境标签列表
summary: 该接口为灰度网关提供用户的灰度信息,用于服务端灰度。获取指定 uid 用户在指定 product 产品下的所有(未分页,最多 400 条)环境标签,包括从 group 群组继承的环境标签,按照 label 指派时间反序。网关只会取匹配 client 和 channel 的第一条。标签列表不是实时数据,会被服务缓存,缓存时间在 config.cache_label_expire 配置,默认为 1 分钟,建议生产配置为 5 分钟。当 uid 对应用户不存在或 product 对应产品不存在时,该接口会返回空环境标签列表。当 uid 对应的用户不存在但以 `anon-` 开头时则为匿名用户,百分比发布规则对匿名用户生效。
parameters:
- $ref: "#/components/parameters/PathUID"
- $ref: "#/components/parameters/QueryProduct"
Expand Down Expand Up @@ -1527,7 +1527,7 @@ paths:
get:
tags:
- User
summary: 该接口为客户端提供用户的产品功能模块配置项信息,用于客户端功能灰度。获取指定 uid 用户在指定 product 产品下的功能模块配置项信息列表,包括从 group 群组继承的配置项信息列表,按照 setting 值更新时间 updatedAt 反序。该 API 支持分页,默认获取最新更新的前 10 条,分页参数 nextPageToken 为更新时间 updatedAt 值(进行了 encodeURI 转义)。如果客户端本地缓存了 setting 列表,可以判断 nextPageToken 的值,如果 **为空** 或者其值小于本地缓存的最大 updatedAt 值,就不用读取下一页了。该 API 还支持 channel 和 client 参数,让客户端只读取匹配 client 和 channel 的 setting 列表。当 uid 对应用户不存在时,该接口会返回空配置项列表
summary: 该接口为客户端提供用户的产品功能模块配置项信息,用于客户端功能灰度。获取指定 uid 用户在指定 product 产品下的功能模块配置项信息列表,包括从 group 群组继承的配置项信息列表,按照 setting 值更新时间 updatedAt 反序。该 API 支持分页,默认获取最新更新的前 10 条,分页参数 nextPageToken 为更新时间 updatedAt 值(进行了 encodeURI 转义)。如果客户端本地缓存了 setting 列表,可以判断 nextPageToken 的值,如果 **为空** 或者其值小于本地缓存的最大 updatedAt 值,就不用读取下一页了。该 API 还支持 channel 和 client 参数,让客户端只读取匹配 client 和 channel 的 setting 列表。当 uid 对应用户不存在时,该接口会返回空配置项列表。当 uid 对应的用户不存在但以 `anon-` 开头时则为匿名用户,百分比发布规则对匿名用户生效。
security:
- HeaderAuthorizationJWT: {}
parameters:
Expand Down Expand Up @@ -1589,6 +1589,8 @@ paths:
- $ref: "#/components/parameters/QueryProduct"
- $ref: "#/components/parameters/QueryModule"
- $ref: "#/components/parameters/QuerySetting"
- $ref: "#/components/parameters/QueryChannel"
- $ref: "#/components/parameters/QueryClient"
- $ref: "#/components/parameters/QueryPageSize"
- $ref: "#/components/parameters/QueryPageToken"
- $ref: "#/components/parameters/QueryQ"
Expand Down Expand Up @@ -1655,6 +1657,8 @@ paths:
- $ref: "#/components/parameters/QueryProduct"
- $ref: "#/components/parameters/QueryModule"
- $ref: "#/components/parameters/QuerySetting"
- $ref: "#/components/parameters/QueryChannel"
- $ref: "#/components/parameters/QueryClient"
- $ref: "#/components/parameters/QueryPageSize"
- $ref: "#/components/parameters/QueryPageToken"
- $ref: "#/components/parameters/QueryQ"
Expand Down
2 changes: 2 additions & 0 deletions doc/paths_group.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
- $ref: "#/components/parameters/QueryProduct"
- $ref: "#/components/parameters/QueryModule"
- $ref: "#/components/parameters/QuerySetting"
- $ref: "#/components/parameters/QueryChannel"
- $ref: "#/components/parameters/QueryClient"
- $ref: "#/components/parameters/QueryPageSize"
- $ref: "#/components/parameters/QueryPageToken"
- $ref: "#/components/parameters/QueryQ"
Expand Down
6 changes: 4 additions & 2 deletions doc/paths_user.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
get:
tags:
- User
summary: 该接口为灰度网关提供用户的灰度信息,用于服务端灰度。获取指定 uid 用户在指定 product 产品下的所有(未分页,最多 400 条)环境标签,包括从 group 群组继承的环境标签,按照 label 指派时间反序。网关只会取匹配 client 和 channel 的第一条。标签列表不是实时数据,会被服务缓存,缓存时间在 config.cache_label_expire 配置,默认为 1 分钟,建议生产配置为 5 分钟。当 uid 对应用户不存在或 product 对应产品不存在时,该接口会返回空环境标签列表
summary: 该接口为灰度网关提供用户的灰度信息,用于服务端灰度。获取指定 uid 用户在指定 product 产品下的所有(未分页,最多 400 条)环境标签,包括从 group 群组继承的环境标签,按照 label 指派时间反序。网关只会取匹配 client 和 channel 的第一条。标签列表不是实时数据,会被服务缓存,缓存时间在 config.cache_label_expire 配置,默认为 1 分钟,建议生产配置为 5 分钟。当 uid 对应用户不存在或 product 对应产品不存在时,该接口会返回空环境标签列表。当 uid 对应的用户不存在但以 `anon-` 开头时则为匿名用户,百分比发布规则对匿名用户生效。
parameters:
- $ref: "#/components/parameters/PathUID"
- $ref: "#/components/parameters/QueryProduct"
Expand Down Expand Up @@ -31,7 +31,7 @@
get:
tags:
- User
summary: 该接口为客户端提供用户的产品功能模块配置项信息,用于客户端功能灰度。获取指定 uid 用户在指定 product 产品下的功能模块配置项信息列表,包括从 group 群组继承的配置项信息列表,按照 setting 值更新时间 updatedAt 反序。该 API 支持分页,默认获取最新更新的前 10 条,分页参数 nextPageToken 为更新时间 updatedAt 值(进行了 encodeURI 转义)。如果客户端本地缓存了 setting 列表,可以判断 nextPageToken 的值,如果 **为空** 或者其值小于本地缓存的最大 updatedAt 值,就不用读取下一页了。该 API 还支持 channel 和 client 参数,让客户端只读取匹配 client 和 channel 的 setting 列表。当 uid 对应用户不存在时,该接口会返回空配置项列表
summary: 该接口为客户端提供用户的产品功能模块配置项信息,用于客户端功能灰度。获取指定 uid 用户在指定 product 产品下的功能模块配置项信息列表,包括从 group 群组继承的配置项信息列表,按照 setting 值更新时间 updatedAt 反序。该 API 支持分页,默认获取最新更新的前 10 条,分页参数 nextPageToken 为更新时间 updatedAt 值(进行了 encodeURI 转义)。如果客户端本地缓存了 setting 列表,可以判断 nextPageToken 的值,如果 **为空** 或者其值小于本地缓存的最大 updatedAt 值,就不用读取下一页了。该 API 还支持 channel 和 client 参数,让客户端只读取匹配 client 和 channel 的 setting 列表。当 uid 对应用户不存在时,该接口会返回空配置项列表。当 uid 对应的用户不存在但以 `anon-` 开头时则为匿名用户,百分比发布规则对匿名用户生效。
security:
- HeaderAuthorizationJWT: {}
parameters:
Expand Down Expand Up @@ -93,6 +93,8 @@
- $ref: "#/components/parameters/QueryProduct"
- $ref: "#/components/parameters/QueryModule"
- $ref: "#/components/parameters/QuerySetting"
- $ref: "#/components/parameters/QueryChannel"
- $ref: "#/components/parameters/QueryClient"
- $ref: "#/components/parameters/QueryPageSize"
- $ref: "#/components/parameters/QueryPageToken"
- $ref: "#/components/parameters/QueryQ"
Expand Down
21 changes: 21 additions & 0 deletions src/api/label_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,27 @@ func TestLabelAPIs(t *testing.T) {
assert.Equal(label.Name, data.Label)
})

t.Run(`"GET /users/:uid/labels:cache" should support anonymous user`, func(t *testing.T) {
assert := assert.New(t)
res, err := request.Get(fmt.Sprintf("%s/users/%s/labels:cache?product=%s", tt.Host, "anon-"+user.UID, product.Name)).
End()
assert.Nil(err)
assert.Equal(200, res.StatusCode)

text, err := res.Text()
assert.Nil(err)
assert.False(strings.Contains(text, `"id"`))

json := tpl.CacheLabelsInfoRes{}
_, err = res.JSON(&json)

assert.Nil(err)
assert.Equal(1, len(json.Result))

data := json.Result[0]
assert.Equal(label.Name, data.Label)
})

t.Run(`"GET /v1/products/:product/labels/:label/rules" should work`, func(t *testing.T) {
assert := assert.New(t)
res, err := request.Get(fmt.Sprintf("%s/v1/products/%s/labels/%s/rules", tt.Host, product.Name, label.Name)).
Expand Down
26 changes: 26 additions & 0 deletions src/api/setting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,32 @@ func TestSettingAPIs(t *testing.T) {
assert.True(data.AssignedAt.After(time2020))
})

t.Run(`"GET /v1/users/:uid/settings:unionAll" should support anonymous user`, func(t *testing.T) {
assert := assert.New(t)
res, err := request.Get(fmt.Sprintf("%s/v1/users/%s/settings:unionAll?product=%s", tt.Host, "anon-"+user.UID, product.Name)).
End()
assert.Nil(err)
assert.Equal(200, res.StatusCode)

text, err := res.Text()
assert.Nil(err)
assert.False(strings.Contains(text, `"id"`))

json := tpl.MySettingsRes{}
_, err = res.JSON(&json)

assert.Nil(err)
assert.Equal(1, len(json.Result))
assert.Equal("", json.NextPageToken)

data := json.Result[0]
assert.Equal(service.IDToHID(setting.ID, "setting"), data.HID)
assert.Equal(module.Name, data.Module)
assert.Equal(setting.Name, data.Name)
assert.Equal("y", data.Value)
assert.True(data.AssignedAt.After(time2020))
})

t.Run(`"GET /v1/products/:product/modules/:module/settings/:setting/rules" should work`, func(t *testing.T) {
assert := assert.New(t)
res, err := request.Get(fmt.Sprintf("%s/v1/products/%s/modules/%s/settings/%s/rules", tt.Host, product.Name, module.Name, setting.Name)).
Expand Down
2 changes: 1 addition & 1 deletion src/bll/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (b *Group) ListSettings(ctx context.Context, req tpl.MySettingsQueryURL) (*
}

pg := req.Pagination
settings, total, err := b.ms.Group.FindSettings(ctx, group.ID, productID, moduleID, settingID, pg)
settings, total, err := b.ms.Group.FindSettings(ctx, group.ID, productID, moduleID, settingID, pg, req.Channel, req.Client)
if err != nil {
return nil, err
}
Expand Down
34 changes: 28 additions & 6 deletions src/bll/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bll

import (
"context"
"strings"
"time"

"github.com/teambition/urbs-setting/src/conf"
Expand Down Expand Up @@ -42,12 +43,23 @@ func (b *User) List(ctx context.Context, pg tpl.Pagination) (*tpl.UsersRes, erro
func (b *User) ListCachedLabels(ctx context.Context, uid, product string) *tpl.CacheLabelsInfoRes {
now := time.Now().UTC()
res := &tpl.CacheLabelsInfoRes{Result: []schema.UserCacheLabel{}, Timestamp: now.Unix()}

if product == "" {
return res
}

productID, err := b.ms.Product.AcquireID(ctx, product)
if err != nil {
return res
}

user, err := b.ms.User.Acquire(ctx, uid)
if err != nil {
if strings.HasPrefix(uid, "anon-") {
if labels, err := b.ms.LabelRule.ApplyRulesToAnonymous(ctx, uid, productID); err == nil {
res.Result = labels
}
}
return res
}

Expand Down Expand Up @@ -133,7 +145,7 @@ func (b *User) ListSettings(ctx context.Context, req tpl.MySettingsQueryURL) (*t
}

pg := req.Pagination
settings, total, err := b.ms.User.FindSettings(ctx, userID, productID, moduleID, settingID, pg)
settings, total, err := b.ms.User.FindSettings(ctx, userID, productID, moduleID, settingID, pg, req.Channel, req.Client)
if err != nil {
return nil, err
}
Expand All @@ -150,18 +162,28 @@ func (b *User) ListSettings(ctx context.Context, req tpl.MySettingsQueryURL) (*t
// ListSettingsUnionAll ...
func (b *User) ListSettingsUnionAll(ctx context.Context, req tpl.MySettingsQueryURL) (*tpl.MySettingsRes, error) {
res := &tpl.MySettingsRes{Result: []tpl.MySetting{}}
user, err := b.ms.User.Acquire(ctx, req.UID)
if err != nil {
return res, nil
}

var productID int64
var moduleID int64
var settingID int64
productID, err = b.ms.Product.AcquireID(ctx, req.Product)
productID, err := b.ms.Product.AcquireID(ctx, req.Product)
if err != nil {
return nil, err
}

user, err := b.ms.User.Acquire(ctx, req.UID)
if err != nil {
if strings.HasPrefix(req.UID, "anon-") {
if settings, err := b.ms.SettingRule.ApplyRulesToAnonymous(ctx, req.UID, productID, req.Channel, req.Client); err == nil {
for i := range settings {
settings[i].Product = req.Product
}
res.Result = settings
}
}
return res, nil
}

if req.Module != "" {
moduleID, err = b.ms.Module.AcquireID(ctx, productID, req.Module)
if err != nil {
Expand Down
12 changes: 11 additions & 1 deletion src/model/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func (m *Group) FindLabels(ctx context.Context, groupID int64, pg tpl.Pagination
}

// FindSettings 根据 Group ID, updateGt, productName 返回其 settings 数据。
func (m *Group) FindSettings(ctx context.Context, groupID, productID, moduleID, settingID int64, pg tpl.Pagination) ([]tpl.MySetting, int, error) {
func (m *Group) FindSettings(ctx context.Context, groupID, productID, moduleID, settingID int64, pg tpl.Pagination, channel, client string) ([]tpl.MySetting, int, error) {
data := []tpl.MySetting{}
cursor := pg.TokenToID()

Expand Down Expand Up @@ -196,6 +196,16 @@ func (m *Group) FindSettings(ctx context.Context, groupID, productID, moduleID,
sd = sd.Where(goqu.I("t1.setting_id").Eq(goqu.I("t2.id")))
}

if channel != "" {
sdc = sdc.Where(goqu.L("FIND_IN_SET(?, ?)", channel, goqu.I("t2.channels")))
sd = sd.Where(goqu.L("FIND_IN_SET(?, ?)", channel, goqu.I("t2.channels")))
}

if client != "" {
sdc = sdc.Where(goqu.L("FIND_IN_SET(?, ?)", client, goqu.I("t2.clients")))
sd = sd.Where(goqu.L("FIND_IN_SET(?, ?)", client, goqu.I("t2.clients")))
}

if pg.Q != "" {
sdc = sdc.Where(goqu.I("t2.name").ILike(pg.Q))
sd = sd.Where(goqu.I("t2.name").ILike(pg.Q))
Expand Down
69 changes: 69 additions & 0 deletions src/model/label_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package model

import (
"context"
"hash/crc32"
"time"

"github.com/doug-martin/goqu/v9"
Expand Down Expand Up @@ -62,6 +63,74 @@ func (m *LabelRule) ApplyRules(ctx context.Context, userID int64, excludeLabels
return len(ids), nil
}

// ApplyRulesToAnonymous ...
func (m *LabelRule) ApplyRulesToAnonymous(ctx context.Context, anonymousID string, productID int64) ([]schema.UserCacheLabel, error) {
rules := []schema.LabelRule{}
sd := m.DB.From(schema.TableLabelRule).
Where(
goqu.C("kind").Eq("userPercent"),
goqu.C("product_id").Eq(productID)).
Order(goqu.C("updated_at").Desc()).Limit(200)
err := sd.Executor().ScanStructsContext(ctx, &rules)
if err != nil {
return nil, err
}

anonID := int64(crc32.ChecksumIEEE([]byte(anonymousID)))
labelIDs := make([]int64, 0)
for _, rule := range rules {
p := rule.ToPercent()
if p > 0 && (int((anonID+rule.CreatedAt.Unix())%100) <= p) {
// 百分比规则无效或者用户不在百分比区间内
labelIDs = append(labelIDs, rule.LabelID)
}
}

data := make([]schema.UserCacheLabel, 0)
if len(labelIDs) > 0 {
sd := m.DB.Select(
goqu.I("t1.id"),
goqu.I("t1.name"),
goqu.I("t1.channels"),
goqu.I("t1.clients")).
From(goqu.T(schema.TableLabel).As("t1")).
Where(goqu.I("t1.id").In(labelIDs))

scanner, err := sd.Executor().ScannerContext(ctx)
if err != nil {
return nil, err
}

mp := make(map[int64]schema.UserCacheLabel)
for scanner.Next() {
myLabelInfo := schema.MyLabelInfo{}
if err := scanner.ScanStruct(&myLabelInfo); err != nil {
scanner.Close()
return nil, err
}

mp[myLabelInfo.ID] = schema.UserCacheLabel{
Label: myLabelInfo.Name,
Clients: tpl.StringToSlice(myLabelInfo.Clients),
Channels: tpl.StringToSlice(myLabelInfo.Channels),
}
}

scanner.Close()
if err := scanner.Err(); err != nil {
return nil, err
}

for _, id := range labelIDs {
if label, ok := mp[id]; ok {
data = append(data, label)
}
}
}

return data, nil
}

// Acquire ...
func (m *LabelRule) Acquire(ctx context.Context, labelRuleID int64) (*schema.LabelRule, error) {
labelRule := &schema.LabelRule{}
Expand Down
Loading

0 comments on commit 44a0f02

Please sign in to comment.